Skip to Content

Digitale Uhr mit CrowPanel 3.5″ ESP32 Display

Digitale Uhr mit CrowPanel 3.5″ ESP32 Display

In diesem Tutorial lernst du, wie du mit dem CrowPanel 3.5″ ESP32 Display eine immer genaue Digitale Uhr baust. Wir synchronisieren unsere Uhr per WLAN mit einem Internet-Zeitserver und nutzen die TFT_eSPI Bibliothek, um eine ansprechende Benutzeroberfläche für das Display zu erstellen.

Los geht’s!

Benötigte Teile

Für dieses Projekt benötigst du nur das CrowPanel 3.5″ ESP32 Display von ELECROW und die Arduino IDE. Das Panel wird mit einem USB-Kabel und einem 4-poligen DuPont-Kabel geliefert, du brauchst also keine zusätzlichen Kabel.

CrowPanel 3.5″ ESP32 Display

Makerguides is a participant in affiliate advertising programs designed to provide a means for sites to earn advertising fees by linking to Amazon, AliExpress, Elecrow, and other sites. As an Affiliate we may earn from qualifying purchases.

Eigenschaften des CrowPanel 3.5″ ESP32 Displays

Das CrowPanel 3.5″ ESP32 Display von ELECROW ist ein resistiver Touchscreen mit einem 480*320 Pixel TFT-Display und einem integrierten ESP32-WROVER-B als Steuerprozessor. Du kannst das Display mit einem schicken Acrylgehäuse bekommen (siehe Bild unten), sodass du kein eigenes Gehäuse bauen musst.

CrowPanel 3.5" ESP32 Display with Housing
CrowPanel 3.5″ ESP32 Display mit Gehäuse (source)

Darüber hinaus verfügt das Board über einen TF-Karten-Slot, eine UART-Schnittstelle, eine I2C-Schnittstelle, einen Lautsprecheranschluss, einen Batterieanschluss mit Ladefunktion und einen kleinen GPIO-Port mit zwei GPIO-Pins. Siehe das Pinout unten:

Pinout of CrowPanel 3.5" ESP32 Display
Pinout des CrowPanel 3.5″ ESP32 Displays

Die folgende Tabelle zeigt, welche GPIO-Pins den drei IO-Schnittstellen zugeordnet sind.

GPIO_DIO25; IO32
UARTRX(IO16); TX(IO17)
I2CSDA(IO22); SCL(IO21)

Das Board kann entweder über den USB-Anschluss (5V, 2A) oder durch Anschluss eines Standard-3,7V-LiPo-Akkus am BAT-Anschluss mit Strom versorgt werden. Achte dabei auf die Polarität des Steckers und des Akkus. Wenn USB-Kabel und Akku gleichzeitig angeschlossen sind, lädt das Board den Akku (maximaler Ladestrom 500mA).

Du kannst das Board mit verschiedenen Entwicklungsumgebungen programmieren, wie Arduino IDE, Espressif IDF, Lua RTOS und Micro Python. Es ist kompatibel mit der LVGL Grafikbibliothek. In diesem Tutorial verwenden wir jedoch die Arduino IDE und die TFT_eSPI Grafikbibliothek, um die Benutzeroberfläche für unsere Uhr zu erstellen.

CrowPanel ESP32 Display Serie

Beachte, dass das CrowPanel 3.5″ ESP32 Display Teil einer ganzen Familie von Displays ist, die von 2,4 Zoll bis 7 Zoll reichen. Neben der Größe unterscheiden sie sich auch in Auflösung, Display-Treiber und ESP32-Modell. Siehe die Tabelle unten.

CrowPanel ESP32 Displays (source)

In diesem Tutorial verwenden wir 3.5″ display. Die Codebeispiele und das Setup sind aber sehr ähnlich für das 2.4″ display und das 2.8“ display, da sie den gleichen oder einen ähnlichen Display-Treiber (ILI9341, ILI9488) nutzen. Siehe die gelb markierten Bereiche in der Tabelle oben.

Für die größeren Displays (4,3″, 5″, 7″) werden die Codebeispiele allerdings wahrscheinlich nicht funktionieren, da die TFT_eSPI Bibliothek deren Treiber (NV3047, IL6122, EK9716BD3) anscheinend nicht unterstützt. Ich habe das aber nicht selbst getestet. Schreib gerne einen Kommentar, falls du es ausprobiert hast.

Projektstruktur anlegen

Bevor wir ins Detail gehen, legen wir die Projektstruktur und das Setup für die TFT_eSPI Bibliothek an.

Öffne deine Arduino IDE und erstelle ein Projekt mit dem Namen „digiclock“ und speichere es (Speichern unter …). Dadurch wird ein Ordner „digiclock“ mit der Datei „digiclock.ino“ darin angelegt. In diesem Ordner erstellst du eine weitere Datei mit dem Namen „tft_setup.h„. Dein Projektordner sollte nun so aussehen:

Project Folder Structure
Projektordner-Struktur

Und in deiner Arduino IDE solltest du jetzt zwei Tabs mit den Namen „digiclock.ino“ und „tft_setup.h“ sehen.

Arduino IDE with two Tabs
Arduino IDE mit zwei Tabs

Klicke auf den Tab „tft_setup.h„, um die Datei zu öffnen, und kopiere folgenden Code hinein:

#define ILI9488_DRIVER
#define TFT_WIDTH  480
#define TFT_HEIGHT 320 

#define TFT_BACKLIGHT_ON HIGH
#define TFT_BL   27 
#define TFT_MISO 12
#define TFT_MOSI 13
#define TFT_SCLK 14
#define TFT_CS   15
#define TFT_DC    2 
#define TFT_RST  -1
#define TOUCH_CS 33

#define SPI_FREQUENCY        27000000
#define SPI_TOUCH_FREQUENCY   2500000
#define SPI_READ_FREQUENCY   16000000

#define LOAD_GLCD   // Font 1. Original Adafruit 8 pixel font needs ~1820 bytes in FLASH
#define LOAD_FONT2  // Font 2. Small 16 pixel high font, needs ~3534 bytes in FLASH, 96 characters
#define LOAD_FONT4  // Font 4. Medium 26 pixel high font, needs ~5848 bytes in FLASH, 96 characters
#define LOAD_FONT6  // Font 6. Large 48 pixel font, needs ~2666 bytes in FLASH, only characters 1234567890:-.apm
#define LOAD_FONT7  // Font 7. 7 segment 48 pixel font, needs ~2438 bytes in FLASH, only characters 1234567890:-.
#define LOAD_FONT8  // Font 8. Large 75 pixel font needs ~3256 bytes in FLASH, only characters 1234567890:-.
#define LOAD_GFXFF  // FreeFonts. Include access to the 48 Adafruit_GFX free fonts FF1 to FF48 and custom fonts

Dieser Code teilt der TFT_eSPI Bibliothek mit, welches Display wir verwenden. Konkret geben wir der Bibliothek die Abmessungen des Displays (TFT_WIDTH, TFT_HEIGHT), den Display-Treiber (ILI9488_DRIVER), welche SPI-Pins zur Steuerung verwendet werden und welche Fonts geladen werden sollen. Ohne diese Einstellungen kannst du nichts auf dem Display anzeigen.

Für mehr Details schau dir das Tutorial CrowPanel 2.8″ ESP32 Display : Easy Setup Guide an. Dort erklären wir, wie man das 2,8″-Display einrichtet. Die Schritte und Beschreibungen gelten aber genauso für das 3,5″-Display. Nur die Einstellungen für Bildschirmgröße und Treiber sind unterschiedlich.

Touchscreen kalibrieren

Das CrowPanel 3.5″ Display hat einen resistiven Touchscreen, den du zuerst kalibrieren musst, bevor du ihn mit der TFT_eSPI Bibliothek nutzen kannst. Kopiere den folgenden Code in die Datei „digiclock.ino„, kompiliere ihn und lade ihn auf das CrowPanel 3.5“ Display hoch.

// digiclock.ino

#include "tft_setup.h"
#include "TFT_eSPI.h"

TFT_eSPI tft = TFT_eSPI();

void setup() {
  Serial.begin(115200);
  tft.begin();
  tft.setRotation(0);
}

void loop() {
  uint16_t cal[5];
  tft.setRotation(1);  // Landscape orientation!
  tft.fillScreen(TFT_BLACK);
  tft.setCursor(20, 0);
  tft.setTextFont(2);
  tft.setTextSize(1);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.print("Touch corners ... ");
  tft.calibrateTouch(cal, TFT_MAGENTA, TFT_BLACK, 15);
  tft.println("done.");

  Serial.printf("cal: {%d, %d, %d, %d, %d}\n",
                cal[0], cal[1], cal[2], cal[3], cal[4]);
  delay(10000);
}

Wenn der Code läuft, zeigt das Display einen Pfeil und fordert dich auf, die Ecke zu berühren, auf die er zeigt. Das wird für die anderen drei Ecken wiederholt. Benutze den kleinen Stift, der dem Display beiliegt, und versuche, die Ecken möglichst genau zu treffen.

Calibration of touch screen
Kalibrierung des Touchscreens

Am Ende des Kalibrierungsprozesses gibt der Code die 5 Kalibrierungsparameter (Eckkoordinaten und Bildschirmausrichtung) im Serial Monitor aus. Du solltest etwas wie das Folgende sehen:

cal: { 243, 3669, 216, 3553, 7 }

Kopiere die Parameter irgendwohin, da du sie später im Uhren-Code brauchst. Die Kalibrierung wiederholt sich alle 10 Sekunden, sodass du mehrere Versuche hast, um die genauesten Werte zu bekommen.

Für mehr Details zum Kalibrieren schau dir das CrowPanel 2.8″ ESP32 Display : Easy Setup Guide Tutorial an. Beachte aber, dass dort die Kalibrierung im Hochformat gemacht wird, während wir hier das Display im Querformat nutzen (siehe setRotation(1) im Kalibrierungscode oben).

Als nächstes schauen wir uns an, wie wir die Zeitinformation aus dem Internet bekommen.

Zeitdaten aus dem Internet laden

Ich mag Uhren, die sich automatisch an die Sommerzeit anpassen und immer genau sind, sodass ich sie nicht stellen muss. Wenn du WLAN und Internet hast, ist der einfachste Weg, regelmäßig die aktuelle Zeit von einem Zeitserver zu laden und dann die interne Uhr des ESP32 anzupassen. Für mehr Infos dazu schau dir das Tutorial zum Bau einer Automatic Daylight Savings Time Clock an.

Hier verwenden wir die gleiche Methode wie in diesem Tutorial. Wir nutzen die WorldTimeAPI, um unsere Zeitdaten zu bekommen. WorldTimeAPI ist ein einfacher Webservice, der die aktuelle Zeit als Klartext oder JSON zurückgibt. Du kannst die Website nutzen, um die Zeit für eine bestimmte Zeitzone oder basierend auf der IP-Adresse deines Computers zu bekommen. Wir nutzen Letzteres. Das ist einfacher und unsere Uhr passt sich dann nicht nur automatisch an die Sommerzeit an, sondern auch, wenn sie in eine andere Zeitzone gebracht wird.

WorldTimeAPI kannst du ganz einfach ausprobieren. Klicke einfach auf diesen Link: http://worldtimeapi.org/api/ip oder gib ihn in die Adresszeile deines Browsers ein. Du musst deine IP-Adresse nicht explizit angeben. Der Webservice erkennt das automatisch anhand der Herkunft der Anfrage.

Im Browser solltest du eine Ausgabe ähnlich der folgenden sehen (ich habe sie etwas formatiert und meine IP-Adresse unkenntlich gemacht):

{
    "abbreviation": "AEDT",
    "client_ip": "122.150.000.000",
    "datetime": "2023-11-16T12:09:46.409360+11:00",
    "day_of_week": 4,
    "day_of_year": 320,
    "dst": true,
    "dst_from": "2023-09-30T16:00:00+00:00",
    "dst_offset": 3600,
    "dst_until": "2024-04-06T16:00:00+00:00",
    "raw_offset": 36000,
    "timezone": "Australia/Melbourne",
    "unixtime": 1700096986,
    "utc_datetime": "2023-11-16T01:09:46.409360+00:00",
    "utc_offset": "+11:00",
    "week_number": 46
}

Diese Ausgabe ist im JSON-Format. Sie enthält noch weitere Informationen außer der aktuellen Zeit. Uns interessiert aber speziell das datetime Feld, das uns die aktuelle lokale Zeit liefert. In diesem Beispiel ist es "2023-11-16T12:09:46.409360+11:00".

Jedes Mal, wenn du diesen Link aufrufst, bekommst du die aktuelle Zeit. Ruf den Link aber nicht zu oft auf, sonst wirst du eventuell blockiert!

Damit haben wir alle Bausteine, um unsere Digitale Uhr umzusetzen.

Digitale Uhr umsetzen

In diesem Abschnitt setzen wir die Digitale Uhr um. Sie wird so aussehen:

Example Display of the Digital Clock
Beispielanzeige der Digitaluhr

Sie zeigt die aktuelle (internet-synchronisierte) Uhrzeit, den Tag, Monat und das Jahr sowie zwei Buttons. Mit dem „24h“-Button kannst du zwischen 24h- und 12h-Format umschalten. Mit dem „day“-Button kannst du die Helligkeit des Displays für Tag und Nacht ändern.

Um diese Uhr zu bauen, fangen wir mit der Struktur des Projekts an.

Projektordner-Struktur

Im vorherigen Schritt hast du bereits den Projektordner „digiclock“ mit den Dateien „digiclock.ino“ und „tft_setup.h“ angelegt. Jetzt füge noch eine Datei namens „datestr.h“ hinzu. Dein Projektordner sollte jetzt so aussehen:

Project Folder Structure
Projektordner-Struktur

und deine Arduino IDE sollte jetzt drei Tabs haben: „digiclock.ino„, „datestr.h“ und „tft_setup.h„:

Tabs in Arduino IDE
Tabs in der Arduino IDE

Die Datei „tft_setup.h“ ist bereits mit dem richtigen Code gefüllt. Die anderen beiden Dateien („digiclock.ino„, „datestr.h„) müssen wir noch anpassen. Fangen wir mit der Datei „datestr.h“ an.

Tages- und Monatsnamen

Zusätzlich zur Uhrzeit wollen wir auch das aktuelle Datum als Text auf dem Display anzeigen, z.B. Do, Mai 30, 2024. Von WorldTimeAPI bekommen wir aber keine Namen für den aktuellen Monat oder Tag. Wir müssen also z.B. aus der Monatszahl 5 den Monatsnamen „Mai“ machen.

Die TimeLib Bibliothek, die wir später verwenden, hat zwar Funktionen dafür, aber ich habe sie nicht zum Laufen bekommen. Es gab ein Speicherproblem beim Schreiben in einen String über sprintf, das ich nicht weiter untersuchen wollte.

Deshalb definieren wir unsere eigenen Monats- und Tagesnamen. Das ist einfach und hat den Vorteil, dass du die Namen nach Belieben anpassen kannst. Ganze Namen (Montag), drei Buchstaben (Mon), zwei Buchstaben (MO), Groß- oder Kleinschreibung, andere Sprache – alles ist möglich. Kopiere einfach den folgenden Code in die Datei „datestr.h“ und passe ihn nach Wunsch an.

// datestr.h

const char *DAYSTR[] = {
    "ERROR",
    "Sun",
    "Mon",
    "Tue",
    "Wed",
    "Thu",
    "Fri",
    "Sat"
};

const char *MONTHSTR[] = {
    "ERROR",
    "January",
    "February",
    "March",
    "April",
    "May",
    "June",
    "July",
    "August",
    "September",
    "October",
    "November",
    "December"
};

Hauptprogramm

In diesem Abschnitt implementieren wir das Hauptprogramm, also den Kern unserer Uhr. Kopiere den folgenden Code in die Datei „digiclock.ino„. Ersetze dabei den Kalibrierungscode komplett.

// digiclock.ino

#include "tft_setup.h"
#include "stdarg.h"
#include "WiFi.h"
#include "TFT_eSPI.h"
#include "TFT_eWidget.h"
#include "HTTPClient.h"
#include "ArduinoJson.h"
#include "TimeLib.h"
#include "datestrs.h"

#define WIFI_SSID "YOUR_SSID"
#define WIFI_PASSPHRASE "YOUR_PWD"
#define URL "http://worldtimeapi.org/api/ip"

StaticJsonDocument<2048> doc;
uint16_t cal[5] = { 243, 3669, 216, 3553, 7 };
char timeStr[20];
char dateStr[40];

TFT_eSPI tft = TFT_eSPI();
ButtonWidget btn1 = ButtonWidget(&tft);
ButtonWidget btn2 = ButtonWidget(&tft);
ButtonWidget* btns[] = { &btn1, &btn2 };

bool is24h = true;
bool isDay = true;

void btn1_pressed(void) {
  if (btn1.justPressed()) {
    is24h = !btn1.getState();
    btn1.drawSmoothButton(is24h, 1, TFT_DARKGREY, is24h ? "24h" : "12h");
  }
}

void btn2_pressed(void) {
  if (btn2.justPressed()) {
    isDay = !btn2.getState();
    btn2.drawSmoothButton(isDay, 1, TFT_DARKGREY, isDay ? "day" : "night");
  }
}

void initButtons() {
  uint16_t w = 100;
  uint16_t h = 50;
  uint16_t y = tft.height() - h + 12;
  uint16_t x = tft.width() / 2;
  tft.setTextFont(4);

  btn1.initButtonUL(x - w - 10, y, w, h, TFT_DARKGREY, TFT_BLACK, TFT_DARKGREY, "24h", 1);
  btn1.setPressAction(btn1_pressed);
  btn1.drawSmoothButton(is24h, 1, TFT_BLACK);

  btn2.initButtonUL(x + 10, y, w, h, TFT_DARKGREY, TFT_BLACK, TFT_DARKGREY, "day", 1);
  btn2.setPressAction(btn2_pressed);
  btn2.drawSmoothButton(isDay, 1, TFT_BLACK);
}

void handleButtons() {
  tft.setTextFont(4);
  uint8_t nBtns = sizeof(btns) / sizeof(btns[0]);
  uint16_t x = 0, y = 0;
  bool touched = tft.getTouch(&x, &y);
  for (uint8_t b = 0; b < nBtns; b++) {
    if (touched) {
      if (btns[b]->contains(x, y)) {
        btns[b]->press(true);
        btns[b]->pressAction();
      }
    } else {
      btns[b]->press(false);
      btns[b]->releaseAction();
    }
  }
}

bool shouldSyncTime() {
  time_t t = now();
  bool wifi_on = WiFi.status() == WL_CONNECTED;
  bool should_sync = (minute(t) == 0 && second(t) == 3) || (year(t) == 1970);
  return wifi_on && should_sync;
}

void syncTime() {
  delay(1000);
  HTTPClient http;
  http.begin(URL);
  if (http.GET() > 0) {
    String json = http.getString();
    auto error = deserializeJson(doc, json);
    if (!error) {
      int Y, M, D, h, m, s, ms, tzh, tzm;
      sscanf(doc["datetime"], "%d-%d-%dT%d:%d:%d.%d+%d:%d",
             &Y, &M, &D, &h, &m, &s, &ms, &tzh, &tzm);
      setTime(h, m, s, D, M, Y);
    }
  }
  http.end();
}

void updateDisplay() {
  time_t t = now();
  sprintf(timeStr, " %2d:%02d ",
          (is24h ? hour(t) : hourFormat12(t)), minute(t));
  sprintf(dateStr, "    %s, %s %d, %d    ",
          DAYSTR[weekday(t)], MONTHSTR[month(t)], day(t), year(t));


  uint16_t color = isDay ? TFT_WHITE : TFT_DARKGREY;
  tft.setTextColor(color, TFT_BLACK);
  tft.setTextDatum(MC_DATUM);

  tft.setTextSize(3);
  tft.drawString(timeStr, tft.width() / 2, 120, 7);

  tft.setTextSize(1);
  tft.drawString(dateStr, tft.width() / 2, 230, 4);
}

void setup(void) {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSPHRASE);
  while (WiFi.status() != WL_CONNECTED)
    delay(500);

  tft.init();
  tft.setTouch(cal);
  tft.fillScreen(TFT_BLACK);
  tft.setRotation(1);

  initButtons();
}

void loop() {
  if (shouldSyncTime())
    syncTime();
  updateDisplay();
  handleButtons();
  delay(50);
}

Das ist jetzt eine ganze Menge Code. In den nächsten Abschnitten schauen wir uns die einzelnen Teile an und wie sie zusammenarbeiten.

Der Großteil des Codes basiert auf diesen drei Tutorials: Wie man eine Automatic Daylight Savings Time Clock baut, wie man eine LED Ring Clock with WS2812 baut und das CrowPanel 2.8″ ESP32 Display : Easy Setup Guide. Falls du Schwierigkeiten hast, manche Codeabschnitte oder Erklärungen zu verstehen, schau dort nach.

Bibliotheken

Zuerst binden wir die benötigten Bibliotheken ein. Neben der „TFT_eSPI“ Bibliothek musst du die „install„, die „TFT_eWidget„, die „ArduinoJson“ und die „TimeLib“ Bibliotheken installieren. Nutze dazu einfach wie gewohnt den Library Manager der Arduino IDE.

Die anderen Bibliotheken: „stdarg“, „WiFi“ und „HTTPClient“ sind Teil des ESP32-Cores und müssen nicht separat installiert werden. Und „tft_setup.h“ und „datestrs.h“ sind die Dateien unseres digiclock Arduino-Projekts, die wir vorher angelegt haben. Sie existieren bereits, müssen aber wie gezeigt eingebunden werden.

#include "tft_setup.h"
#include "stdarg.h"
#include "WiFi.h"
#include "TFT_eSPI.h"
#include "TFT_eWidget.h"
#include "HTTPClient.h"
#include "ArduinoJson.h"
#include "TimeLib.h"
#include "datestrs.h"

Diese Bibliotheken stellen die nötigen Funktionen für WLAN, HTTP-Anfragen, JSON-Parsing, Zeitverwaltung und die Benutzeroberfläche unserer Uhr bereit.

Konstanten und Objekte

Als nächstes definieren wir einige Konstanten und Objekte. Das StaticJsonDocument speichert die Antwort der WebTime API auf unsere Zeitabfrage. cal enthält die Kalibrierungsparameter für das Display. Und timeStr und dateStr sind Zeichenpuffer, die wir zum Formatieren der angezeigten Zeit und des Datums verwenden.

StaticJsonDocument<2048> doc;
uint16_t cal[5] = { 243, 3669, 216, 3553, 7 };
char timeStr[20];
char dateStr[40];

TFT_eSPI tft = TFT_eSPI();
ButtonWidget btn1 = ButtonWidget(&tft);
ButtonWidget btn2 = ButtonWidget(&tft);
ButtonWidget* btns[] = { &btn1, &btn2 };

bool is24h = true;
bool isDay = true;

TFT_eSPI ist das Objekt zur Steuerung des Displays und btn1 und btn2 sind die beiden Button-Objekte auf dem Display. Wir speichern die Button-Objekte außerdem in einem Array btns, um die Eventbehandlung zu vereinfachen. Und is24h und isDay sind zwei boolesche Flags, die den Zustand der beiden Buttons repräsentieren.

Button-Funktionen

Die Funktionen btn1_pressed() und btn2_pressed() werden aufgerufen, wenn der jeweilige Button auf dem Touchscreen berührt wird. Sie setzen die Button-Status-Flags is24h und isDay und ändern das Aussehen des Buttons.

void btn1_pressed(void) {
  if (btn1.justPressed()) {
    is24h = !btn1.getState();
    btn1.drawSmoothButton(is24h, 1, TFT_DARKGREY, is24h ? "24h" : "12h");
  }
}

void btn2_pressed(void) {
  if (btn2.justPressed()) {
    isDay = !btn2.getState();
    btn2.drawSmoothButton(isDay, 1, TFT_DARKGREY, isDay ? "day" : "night");
  }
}

Button-Initialisierung

Die Funktion initButtons() erstellt das Anfangsdesign und legt die Position der beiden Buttons auf dem Bildschirm fest.

void initButtons() {
  uint16_t w = 100;
  uint16_t h = 50;
  uint16_t y = tft.height() - h + 12;
  uint16_t x = tft.width() / 2;
  tft.setTextFont(4);

  btn1.initButtonUL(x - w - 10, y, w, h, TFT_DARKGREY, TFT_BLACK, TFT_DARKGREY, "24h", 1);
  btn1.setPressAction(btn1_pressed);
  btn1.drawSmoothButton(is24h, 1, TFT_BLACK);

  btn2.initButtonUL(x + 10, y, w, h, TFT_DARKGREY, TFT_BLACK, TFT_DARKGREY, "day", 1);
  btn2.setPressAction(btn2_pressed);
  btn2.drawSmoothButton(isDay, 1, TFT_BLACK);
}

Wären die Buttons komplett sichtbar, würden sie als abgerundete Rechtecke wie im Bild unten erscheinen:

Original shape of Buttons
Ursprüngliche Form der Buttons

Ich habe die Position aber so gewählt, dass die Buttons halb aus dem Bildschirm herausragen, wodurch sie wie Tabs aussehen:

Partially visible Buttons
Teilweise sichtbare Buttons

Ich finde, das sieht besser aus. Wenn es dir nicht gefällt, ändere einfach die y-Koordinate auf y = tft.height() - h - 10, damit der Button komplett angezeigt wird.

Beachte auch, dass wir tft.setTextFont(4) aufrufen müssen, da die Funktion initButtonUL() keinen Parameter für die Schriftart hat.

Button-Eventhandling

Die Funktion handleButtons() behandelt die Button-Events. Sie geht alle Buttons im btns Array durch. Wenn ein Touch-Event über getTouch() erkannt wird und die x,y-Koordinaten des Events im Button-Bereich liegen (btns[b]->contains(x, y)), wird die entsprechende Button-Funktion aufgerufen.

void handleButtons() {
  tft.setTextFont(4);
  uint8_t nBtns = sizeof(btns) / sizeof(btns[0]);
  uint16_t x = 0, y = 0;
  bool touched = tft.getTouch(&x, &y);
  for (uint8_t b = 0; b < nBtns; b++) {
    if (touched) {
      if (btns[b]->contains(x, y)) {
        btns[b]->press(true);
        btns[b]->pressAction();
      }
    } else {
      btns[b]->press(false);
      btns[b]->releaseAction();
    }
  }
}

Prüfen, ob Zeitsynchronisation nötig ist

Wir wollen die interne Uhr des ESP32 mit der Internetzeit von WorldTimeAPI synchronisieren. Das soll aber nicht zu oft passieren, sonst werden wir von WorldTimeAPI blockiert!

Die Funktion shouldSyncTime() prüft, ob die aktuelle Minute null und 3 Sekunden oder das aktuelle Jahr 1970 ist und gibt dann true zurück, was bedeutet, dass wir die Zeit synchronisieren sollten. Das heißt, wir synchronisieren jede volle Stunde und zwar 3 Sekunden nach der vollen Stunde, um eventuelle Änderungen der Sommerzeit zu erwischen. Im schlimmsten Fall geht unsere Uhr dann 3 Sekunden nach.

bool shouldSyncTime() {
  time_t t = now();
  bool wifi_on = WiFi.status() == WL_CONNECTED;
  bool should_sync = (minute(t) == 0 && second(t) == 3) || (year(t) == 1970);
  return wifi_on && should_sync;
}

Wir prüfen auch, ob das aktuelle Jahr 1970 ist. Das ist das Jahr, das die ESP32-Uhr nach dem Start meldet (Beginn von Unix time). Das bedeutet: Wenn der ESP32 eingeschaltet wird, synchronisieren wir als Erstes die Zeit, auch wenn es nicht genau zur vollen Stunde ist. Sonst könnte unsere Uhr bis zu einer Stunde komplett falsch gehen!

Zeit synchronisieren

Die Funktion syncTime() lädt die aktuelle Zeit von WorldTimeAPI herunter und stellt die interne Uhr des ESP32 entsprechend ein. Dazu macht sie eine GET request zur angegebenen URL und parst die Antwort als JSON mit der ArduinoJson Bibliothek.

Aus der JSON-Struktur werden Jahr, Monat, Tag, Stunde, Minute, Sekunde und Zeitzonen-Informationen extrahiert und die interne ESP32-Zeit mit der Funktion setTime() aus der TimeLib Bibliothek gesetzt.

void syncTime() {
  delay(1000);
  HTTPClient http;
  http.begin(URL);
  if (http.GET() > 0) {
    String json = http.getString();
    auto error = deserializeJson(doc, json);
    if (!error) {
      int Y, M, D, h, m, s, ms, tzh, tzm;
      sscanf(doc["datetime"], "%d-%d-%dT%d:%d:%d.%d+%d:%d",
             &Y, &M, &D, &h, &m, &s, &ms, &tzh, &tzm);
      setTime(h, m, s, D, M, Y);
    }
  }
  http.end();
}

Display aktualisieren

Die Funktion updateDisplay() liest zuerst die aktuelle interne Zeit über now() aus und füllt damit die Puffer timeStr und dateStr mit einer Textdarstellung von Uhrzeit und Datum. Je nach Status is24h des Zeitformat-Buttons wird die Zeit im 24h- oder 12h-Format angezeigt.

Beachte die zusätzlichen Leerzeichen in den Format-Strings, z.B. " %s, %s %d, %d ". Sie sind nötig, da Zeit- und Datumsstrings unterschiedlich lang sind und die Funktion updateDisplay() einfach die aktuelle Anzeige überschreibt, anstatt den Bildschirm zu löschen. Das verhindert Flackern, aber du brauchst die Leerzeichen.

void updateDisplay() {
  time_t t = now();
  sprintf(timeStr, " %2d:%02d ",
          (is24h ? hour(t) : hourFormat12(t)), minute(t));
  sprintf(dateStr, "    %s, %s %d, %d    ",
          DAYSTR[weekday(t)], MONTHSTR[month(t)], day(t), year(t));


  uint16_t color = isDay ? TFT_WHITE : TFT_DARKGREY;
  tft.setTextColor(color, TFT_BLACK);
  tft.setTextDatum(MC_DATUM);

  tft.setTextSize(3);
  tft.drawString(timeStr, tft.width() / 2, 120, 7);

  tft.setTextSize(1);
  tft.drawString(dateStr, tft.width() / 2, 230, 4);
}

Sobald Zeit und Datum in den String-Puffern stehen, zeigen wir sie einfach mit drawString() und den Koordinaten auf dem Bildschirm an. setTextDatum(MC_DATUM) zentriert den Text und setTextColor() verwendet je nach Status isDay des Tag/Nacht-Buttons eine weiße oder dunkelgraue Farbe.

Die updateDisplay()-Funktion zeigt im Wesentlichen vier verschiedene Bildschirme an, je nach Status der is24h und der isDay Flags. Das Bild unten zeigt diese vier Bildschirme:

The four different Screens of the Clock
Die vier verschiedenen Ansichten der Uhr

Durch Kamera und Beleuchtung wirken die Bildschirme bläulich, tatsächlich sind die Farben aber schwarz, weiß und grau. Auch der Unterschied zwischen Nacht- und Tagmodus ist in echt deutlicher.

Beachte, dass die updateDisplay() für andere Bildschirmgrößen nicht ohne Anpassungen funktioniert. Du musst die Schriftgröße anpassen, entweder durch eine andere Schriftart oder -größe. Auch die vertikale Position von Zeit- und Datumsstrings ist fest codiert und müsste angepasst werden.

Setup-Funktion

In der setup() Funktion stellen wir zuerst die WLAN-Verbindung her und initialisieren dann das TFT-Display. Wichtig ist die Kalibrierung des Touchscreens über setTouch(cal), wobei wir die Kalibrierungsparameter verwenden. Ohne das funktionieren die Buttons nicht richtig. Außerdem stellen wir das Display per setRotation(1) auf Querformat.

void setup(void) {
  WiFi.mode(WIFI_STA);
  WiFi.begin(WIFI_SSID, WIFI_PASSPHRASE);
  while (WiFi.status() != WL_CONNECTED)
    delay(500);

  tft.init();
  tft.setTouch(cal);
  tft.fillScreen(TFT_BLACK);
  tft.setRotation(1);

  initButtons();
}

Ich verwende einen schwarzen Hintergrund, aber du kannst auch eine andere Farbe wählen. Dann musst du aber auch die Hintergrundfarbe für Strings und Buttons in updateDisplay() und initButtons() anpassen.

Der letzte Schritt im Setup ist der Aufruf von initButtons(), der die Buttons erstellt und zeichnet.

Loop-Funktion

Mit all den Hilfsfunktionen oben ist die Hauptschleife jetzt ganz einfach. Zuerst prüfen wir, ob wir die ESP32-Uhr mit der Internetzeit synchronisieren sollen. Falls ja, rufen wir syncTime() dafür auf.

void loop() {
  if (shouldSyncTime())
    syncTime();
  updateDisplay();
  handleButtons();
  delay(50);
}

Danach aktualisieren wir einfach das Display mit den aktuellen Zeitdaten und rufen dann handleButtons() auf, um die Button-Events zu behandeln. Die Schleife läuft alle 50ms, was schnell genug ist, um auf Button-Drücke zu reagieren. Die anderen Funktionen könnten auch seltener laufen, z.B. alle 100 bis 500ms, aber die höhere Geschwindigkeit schadet nicht.

Und das war’s! Eine immer genaue, coole kleine Uhr!

Fazit

Die Digitale Uhr, die wir gebaut haben, funktioniert super, aber hier sind ein paar Ideen, wie du sie noch besser machen kannst.

Zum Beispiel könntest du statt der manuellen Helligkeitsänderung einen Fotowiderstand (LDR) einbauen, um die Helligkeit je nach Umgebungslicht automatisch zu regeln. Außerdem macht es wenig Sinn, die Zeit anzuzeigen, wenn niemand da ist. Mit einem PIR-Sensor könntest du Bewegungen erkennen und die Uhr nur aktivieren, wenn jemand im Raum ist. Mehr dazu findest du im Tutorial LED Ring Clock with WS2812.

Du könntest weitere Buttons hinzufügen, um das Datumsformat zu ändern oder externe Beleuchtung zu schalten. Schau dir das CrowPanel 2.8″ ESP32 Display : Easy Setup Guide an, wo wir zwei LEDs vom Display aus steuern. Die WorldTimeAPI erlaubt auch die Angabe von Zeitzonen, sodass du Buttons zum Umschalten zwischen Zeitzonen oder Städten einbauen könntest.

Alternativ zur WorldTimeAPI könntest du auch einen SNTP-Server nutzen, um die Zeit häufiger zu synchronisieren. Sieh dir dazu das How to synchronize ESP32 clock with SNTP server Tutorial an.

Da das CrowPanel eine I2C-Schnittstelle hat, kannst du ganz einfach einen Temperatur-, Feuchtigkeits- oder Luftqualitätssensor anschließen und weitere Infos auf dem Display anzeigen. Sieh dir dazu die Tutorials AM2320 digital temperature and humidity sensor Arduino tutorial und Interfacing Arduino and SGP30 Versatile Air Quality Sensor für mehr Details an.

Schließlich kannst du neben der Zeit auch aktuelle Wetterdaten aus dem Internet laden und anzeigen. Schau mal bei OpenWeather vorbei. Dort gibt es einen kostenlosen Plan mit genug Abrufen für stündliche Wetterupdates.

Wenn du noch mehr Ideen oder Fragen hast, schreib gerne einen Kommentar. Viel Spaß beim Basteln ; )

Links

Hier ein paar Links, die mir beim Schreiben dieses Beitrags geholfen haben.