Skip to Content

Digital Clock with CrowPanel 3.5″ ESP32 Display

Digital Clock with CrowPanel 3.5″ ESP32 Display

In this tutorial you will learn how to build an always accurate Digital Clock with the CrowPanel 3.5″ ESP32 Display. We are going to synchronizing our clock via WiFi with an internet time provider and use the TFT_eSPI library to build a nice UI for the Display.

Let’s get started!

Required Parts

For this project you will only need the CrowPanel 3.5″ ESP32 Display from ELECROW and the Arduino IDE. The panel comes with a USB cable and 4pin DuPont cable, so you will not need any extra cables.

CrowPanel 3.5″ ESP32 Display

Makerguides.com is a participant in the Amazon Services LLC Associates Program, an affiliate advertising program designed to provide a means for sites to earn advertising fees by advertising and linking to products on Amazon.com. As an Amazon Associate we earn from qualifying purchases.

Features of the CrowPanel 3.5″ ESP32 Display

The CrowPanel 3.5″ ESP32 Display by ELECROW is a resistive touch screen with a 480*320 resolution TFT display and a built-in ESP32-WROVER-B as control processor. You can get the display with a nice acrylic housing (see picture below), which means you don’t have build a housing yourself.

CrowPanel 3.5" ESP32 Display with Housing
CrowPanel 3.5″ ESP32 Display with Housing (source)

Beyond that, the board comes with a TF card slot, an UART interface, an I2C interface, a speaker interface, a battery connector with charging capability and a small GPIO port with two GPIO pins. See the pinout below:

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

The following table lists which GPIO pins are assigned to which of the three IO interfaces.

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

The board can be powered via the USB port (5V, 2A) or by connecting a standard 3.7V LiPo battery to the BAT connector. Just watch out for the polarity of the connector and the battery. If the USB cable and the battery are connected at the same time, the board will charge the battery (maximum charging current is 500mA).

You can program the board using various development environments such as Arduino IDE, Espressif IDF, Lua RTOS, and Micro Python, and it is compatible with the LVGL graphics library. However, in this tutorial we will use the Arduino IDE and the TFT_eSPI graphics library to create the User Interface for our Clock.

CrowPanel ESP32 Display Series

Note that the CrowPanel 3.5″ ESP32 Display is part of an entire family of displays ranging from 2.4 inch up to 7 inch. Apart from the size difference, they also differ in resolution, display driver and ESP32 model. See the table below.

CrowPanel ESP32 Displays (source)

In this tutorial, we will use 3.5″ display. But the code examples and setup procedure would be very similar for the 2.4″ display and the 2.8” display, since they are using the same or a similar display driver (ILI9341, ILI9488). See the yellow marked sections in the table above.

For the larger displays (4.3″, 5″, 7″), however, the code examples will probably not work as the TFT_eSPI library does not seem to support their drivers (NV3047, IL6122, EK9716BD3). However, I have not actually tested this. Feel free to post in the comment section, if you have a tried this.

Create Project Structure

Before going into any details, let’s create the project structure and the setup for the TFT_eSPI library.

Open your Arduino IDE and create a project “digiclock” and save it (Save As …). This will create a folder “digiclock” with the file “digiclock.ino” in it. In this folder create another file named “tft_setup.h“. Your project folder should look like this:

And in your Arduino IDE you should now have two tabs named “digiclock.ino” and “tft_setup.h“.

Arduino IDE with two Tabs
Arduino IDE with two Tabs

Click on the “tft_setup.h” tab to open the file and copy the following code into it:

#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

This code tells the TFT_eSPI library, which display are we using. Specifically, we let the library know the dimensions of the display (TFT_WIDTH, TFT_WIDTH), its display driver (ILI9488_DRIVER), which SPI pins are used to control it and what fonts to load. Without these settings, you will not be able to show anything on the display.

For more detailed information have a look at the tutorial CrowPanel 2.8″ ESP32 Display : Easy Setup Guide. There we explain how to setup the 2.8″ Display. But the steps and descriptions apply to the 3.5″ Display in the same way. Just the settings for the screen dimensions and driver are different.

Calibrating Touchscreen

The CrowPanel 3.5″ Display comes with a resistive touch screen that you need to calibrate first, before you can used it with the TFT_eSPI library. Copy the code below into the “digiclock.ino” file, compile it and upload it to the CrowPanel 3.5″ Display.

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

When the code is running the displays shows an arrow and tells you to touch the corner it points. This will be repeated for the other three corners as well. Use the little pen that comes with Display to do this and try to touch the corners as precisely as possible.

Calibration of touch screen
Calibration of touch screen

At the end of the calibration process the code prints out the 5 calibration parameters (corner coordinates and screen orientation) to the Serial.monitor. You should see something like this:

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

Copy the parameters somewhere, since you will need them in the clock code. The calibration repeats itself every 10 seconds, so you can have several tries to get the most accurate parameters.

For more detailed information on the calibration process have a look the CrowPanel 2.8″ ESP32 Display : Easy Setup Guide tutorial. Note, however, that there calibration is done for the display in portrait orientation, while here we use the display in landscape mode (see the setRotation(1) in the calibration code above).

Next, let’s discuss how we get time information from the internet.

Downloading Time Data from the Internet

I like clocks that automatically adjust to day light savings time and that are always accurate, so that I don’t have to set them. If you have Wi-Fi and internet, the easiest way to achieve this, is to regularly download the current time from a time server and then adjust the internal clock of the ESP32. For more detailed information on this have a look at the tutorial on how to build an Automatic Daylight Savings Time Clock.

Here we are going to use the same method as in that tutorial. Namely, we will use the WorldTimeAPI to get our time data. WorldTimeAPI is a simple web service, which returns the current time as either plain-text or JSON. You can use their website to get the time for a given timezone or based on the IP address of your computer. We will use the latter. It is easier and our clock will then also automatically adjust not only to DST but also when moved to a different timezone.

It is very easy to try out WorldTimeAPI. Just click on this link: http://worldtimeapi.org/api/ip or enter it in the search bar of your browser. Note that you don’t explicitly have to provide your IP address. The web service figures this out by itself based on where the query originates from.

In your browser you should see an output similar to the following (I added some formatting to make it nicer to read and obfuscated my IP address)

{
    "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
}

This output is in JSON format. It contains other information beyond the current time. But we are specifically interested in the datetime field, which gives us the current local time. In this example it is "2023-11-16T12:09:46.409360+11:00".

Every time you go to this link, you will get an updated time. Just make sure that you don’t hit this link too frequently, otherwise you may get blocked!

With that we have all the bits and pieces to implement our Digital Clock.

Implementing a Digital Clock

In this section we are going to implement the Digital Clock. It will look like this:

Example Display of the Digital Clock
Example Display of the Digital Clock

It shows the current (internet synchronized) time, day, month and year and two buttons. With the “24h” button we can switch between the 24h and the 12h format. And the “day” button allows us to changes the brightness of the display for night and day.

To implement this clock, let’s start with the structure of the project.

Project Folder Structure

In the previous step, you already created the “digiclock” project folder with a “digiclock.ino” and “tft_setup.h” file in it. Now add another file name “datestr.h” to it. Your project folder should look like this:

Project Folder Structure
Project Folder Structure

and your Arduino IDE should now have three tabs: “digiclock.ino“, “datestr.h” and “tft_setup.h“:

Tabs in Arduino IDE
Tabs in Arduino IDE

The “tft_setup.h” file is already filled with the correct code. But the other two files (“digiclock.ino“, “datestr.h“), we have to update. Let’s start with the “datestr.h” file.

Day and Month Names

In addition to the time, we also want to show the current date as text on the display. For instance, Thu, May 30, 2024. However, from WorldTimeAPI we don’t get the names for the current month or day. We have to convert a month number, such as 5 into a month name “May”, for instance.

The TimeLib library, which we are going to use later, actually has functions for this but I could not get them to work. Some memory allocation issue when writing to a string via sprintf, which I didn’t want to spend much time on resolving.

We therefore define our own month and day names. This is easy enough and has the advantage that you can change the names to your liking. Full names (Monday), three letters (Mon), two letters (MO), upper or lower case, different language, … it is all up to you. Just copy the following code into the “datestr.h” file and change it if you like.

// 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"
};

Main Program

In this section we are going to implement the main program, the core of our clock. Copy the following code into the “digiclock.ino” file. Completely replace the calibration code that was in there.

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

Now this is quite a bit of code. In the following sections we take it apart and understand how the bits and pieces work together.

Most of the code is based on these three tutorials: How to build an Automatic Daylight Savings Time Clock, how to build an LED Ring Clock with WS2812, and the CrowPanel 2.8″ ESP32 Display : Easy Setup Guide. If you have difficulties understanding some of the code or explanations below, have a look there.

Libraries

First we include the required libraries. Apart from the “TFT_eSPI” library, you will need to install the “TFT_eWidget“, the “ArduinoJson“, and the “TimeLib” libraries. Just use the library manager of the Arduino IDE as usual.

The other libraries: “stdarg”, “WiFi” and “HTTPClient” are part of the ESP32 core and you do not need to install them separately. And “tft_setup.h” and “datestrs.h” are the files of digiclock Arduino project, which we created before. The already exist but must be included as shown.

#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"

These libraries provide the required functionality for WiFi connectivity, making HTTP requests, parsing JSON, managing time, and creating the User Interface for our clock.

Constants and Objects

Next, we define some constants and objects. The StaticJsonDocument captures the response of the WebTime API to our time request. cal contains the calibration parameters for the display. And timeStr and dateStr are character buffers we will use to format the displayed time and date.

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 is the object to control the display and btn1 and btn2 are the two button objects shown on the display. We also store the button objects in an array btns to simplify the button event handling. And is24h and isDay are two boolean flag that represent the states of the two buttons.

Button Functions

The btn1_pressed() and btn2_pressed() functions are called when the corresponding button on the touch screen is touched. They set the button state flags is24h and isDay and change the appearance of the button.

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 Initialization

The initButtons() functions creates the initial appearance and defines the location of the two buttons on the screen.

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

If displayed completely the buttons would appear as rounded rectangles as in the picture below

Original shape of Buttons
Original shape of Buttons

However, I set the location so that the buttons actually are halfway out of the screen, which makes them look like tabs:

Partially visible Buttons
Partially visible Buttons

I think this looks better but if you don’t like it just change the y-coordinate to y = tft.height() - h - 10 to get the complete button displayed.

Also note that we have to call tft.setTextFont(4), since the initButtonUL() function, does not have a parameter to set a font.

Button Event Handling

The handleButtons() functions, handles the button events. It iterates through all button stored in the btns array. If a touch event is detected via getTouch() and the x,y-coordinates of the event are within the button area (btns[b]->contains(x, y)) the corresponding button function is called.

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

Check for Time Synchronization

We want to synchronize the internal timer of the ESP32 with the internet time that we download from WorldTimeAPI. However, we don’t want to synchronize too often, otherwise WorldTimeAPI will block us!

The shouldSyncTime() function checks if the current minute is zero and 3 seconds or if the current year is 1970 and then returns true, indicating that we should synchronize the time. That means we synchronize every hour and we do it 3 seconds past the full hour to catch the change in daylight saving time that might occur at the full hour. So worst case our clock is 3 seconds off.

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

We also check if the current year is 1970. This is the year that your ESP32 clock will report when just started (start of Unix time). This means, when the ESP32 is powered on the first thing we will do is to synchronize the time, even if we are not at a full hour. Without this our time might be completely wrong for up to an hour!

Synchronize Time

The syncTime() function downloads the current time from WorldTimeAPI and sets the internal clock of the ESP32 accordingly. For that, it makes a GET request to the specified URL and parses the response as JSON using the ArduinoJson library.

From the JSON structure it extracts the year, month, day, hour, minute, second, and time zone information and sets the internal ESP32 time using the setTime() function from the TimeLib library.

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

Update Display

The updateDisplay() function first reads the current internal time via now() and based on that fills the timeStr and dateStr buffers with a text representation of the time and date. Depending on the state is24h of the time format button, time is either written in 24h or 12h format.

Note the extra spaces in the format strings, e.g. " %s, %s %d, %d ". They are necessary since time and date strings are of varying length and the updateDisplay() function simply overwrites the currently displayed time instead of clearing the screen. That avoids flickering but you need the spaces.

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

Once we have the time and date in the string buffers we simply display them on the screen by calling drawString() with the string and coordinates. setTextDatum(MC_DATUM) makes the text centered, and setTextColor() uses a white or dark grey color, depending on the state isDay of the day/night button.

The updateDisplay() function essentially will display four different screens, depending on the state of the is24h and the isDay flags. The image below shows these four screens:

The four different Screens of the Clock
The four different screens of the Clock

Due to the camera and lighting the screens appear blueish in color, while to the eye the actual colors are black, white and grey. Also the difference between night and day mode is more pronounced in reality.

Note that the updateDisplay() will not work for different screen sizes without some changes. You will have to adjust the font size by either using a different font or font size. Also the vertical position of the time and date strings is hard coded and would have to be adjusted.

Setup Function

In the setup() function, we first establish a Wi-Fi connection and then initialize the TFT display. Important is the calibration of the touch screen via setTouch(cal), where we use the calibration parameters. Without that the buttons will not work correctly. Also note that we set the screen to landscape mode via setRotation(1).

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

I use a black background color but you can pick a different one. However, you will also have to adjust the background color for the strings and buttons in updateDisplay() and initButtons().

The last part of the setup is the call to initButtons(), which creates and draws the buttons.

Loop Function

With all the helper functions above, the main loop is now very simple. First we check if we should to synchronize the ESP32 with the internet time. If so, we call syncTime() to do just that.

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

After that we simply update the display with the current time information and then call handleButtons() to handle the button events. The loop runs every 50msec, which is fast enough to react to button presses. The other function could run on a slower frequency, e.g. 100 to 500msec but the faster speed doesn’t do any harm.

And there you have it! An always accurate, cool little clock!

Conclusions

The Digital Clock we implemented works great but here are some things you could add to make it even better.

For instance, instead of manually changing the brightness we could add a photoresistor (LDR) to do this automatically, depending on the ambient light. Also there is little point in showing the time if nobody is around to see it. With a PIR sensor we could detect motion and activate the clock if a person is detected. See the tutorial LED Ring Clock with WS2812 for more info.

You may want to add other buttons to change the date format or to switch external lighting. Have a look at the CrowPanel 2.8″ ESP32 Display : Easy Setup Guide , where we control two LEDs from the Display. The WorldTimeAPI also allows you to specify time zones, so you could have buttons to switch between time zones or cities.

Alternatively, to WorldTimeAPI you could also connect to an SNTP server, which would allow you to synchronize time more frequently. Have a look at the How to synchronize ESP32 clock with SNTP server tutorial.

Since the CrowPanel has an I2C interface you could easily add a temperature, humidity or air quality sensor to show more information on the display. Have a look a the tutorials AM2320 digital temperature and humidity sensor Arduino tutorial and Interfacing Arduino and SGP30 Versatile Air Quality Sensor for more details on this.

Finally, in addition to time information you can also download current weather data from the internet and display them. Have a look at OpenWeather. They have a free plan with enough quota for hourly weather updates.

If you have more ideas or questions feel free to leave a comment. Otherwise have fun ; )

Links

Here some links that I found helpful for writing this post.