Skip to Content

Digital Timer Switch with CrowPanel ESP32 Display

Digital Timer Switch with CrowPanel ESP32 Display

In this tutorial you will learn how to build a Digital Timer Switch with the CrowPanel 3.5″ ESP32 Display. We are going to synchronizing the internal clock with an internet time provider to keep it accurate and use the TFT_eSPI library to build a nice UI that makes it easy to set times.

A Digital Timer Switch is an electronic device that automatically turns other devices on or off at specific times set by the user. It typically has a digital display for programming on/off times. It can be used to control lights, appliances, and other electrical devices at predetermined times. For example, you can use it to turn on lights in your home at sunset and turn them off at bedtime, or to schedule your sprinkler to water your lawn, and then turn them off after a specific duration to avoid overwatering.

Demo of Digital Timer Switch

The Digital Timer Switch you will build in this tutorial will be better than most commercial digital timer switches. It’s time will be always accurate and there will be no need to adjust for daylight savings time. Furthermore, due to the large display it will be easy to set or change schedules.

Required Parts

For this project you will 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.

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 “digital_timer_switch” and save it (Save As …). This will create a folder “digital_timer_switch” with the file “digital_timer_switch.ino” in it. In this folder create another file named “datestrs.h” and a third named “tft_setup.h“. Your project folder should look like this

Project Folder Structure

and in your Arduino IDE you should now have three tabs named “digital_timer_switch.ino“, “datestrs.h” and “tft_setup.h“.

Arduino IDE with three Tabs
Arduino IDE with three Tabs

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

// tft_setup.h
// makerguides.com
#define ILI9488_DRIVER
#define TFT_HEIGHT  480
#define TFT_WIDTH   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 “digital_timer_switch.ino” file, compile it and upload it to the CrowPanel 3.5″ Display.

#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.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 to. 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: { 260, 3518, 270, 3629, 4 }

Copy the parameters somewhere, since you will need them in the digital timer switch 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.

Code for Digital Timer Switch

In this section we will write the complete code for the Digital Timer Switch. Start by copying the following code into the “datestrs.h” file that is in your digital_timer_switch project folder.

// datestrs.h
const char *DAYSTR[] = {
    "Su",
    "Mo",
    "Tu",
    "We",
    "Th",
    "Fr",
    "Sa"
};

const char *MONTHSTR[] = {
    "Jan",
    "Feb",
    "Mar",
    "Apr",
    "May",
    "Jun",
    "Jul",
    "Aug",
    "Sep",
    "Oct",
    "Nov",
    "Dec"
};

Next copy all of the code below into the “digital_timer_switch.ino” file and replace the calibration code in there.

This is the main code. It allows users to set alarms based on a weekly schedule, view the current time, and toggle alarms on and off. The display updates in real-time, and the user can interact with it through touch buttons. Just have a quick read and then we will discuss the details of the code.

// digital_timer_switch.ino
#include "tft_setup.h"
#include "TFT_eSPI.h"
#include "TFT_eWidget.h"
#include "stdarg.h"
#include "WiFi.h"
#include "esp_sntp.h"
#include "datestrs.h"

const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3";
const char* SSID = "SSID";
const char* PWD = "PASSWORD";

const int rs = 24, cs = 7;
const int mx = 60, my = 40;
const int gw = TFT_WIDTH - mx - 30;
const int gh = TFT_HEIGHT - my - 50;
const int cw = gw / cs;
const int ch = gh / rs;

const int alarmPin = 25;

bool schedule[rs][cs];
bool isTimeView = true;
bool isAlarmOn = false;
uint16_t cal[5] = { 260, 3518, 270, 3629, 4 };

TFT_eSPI tft = TFT_eSPI();
ButtonWidget btnView = ButtonWidget(&tft);
ButtonWidget btnAlarm = ButtonWidget(&tft);
ButtonWidget* btns[] = { &btnView, &btnAlarm };


int c2x(int c) {
  return mx + gw * c / cs;
}

int r2y(int r) {
  return my + gh * r / rs;
}

int x2c(int x) {
  return map(x - mx, 0, gw, 0, cs);
}

int y2r(int y) {
  return map(y - my, 0, gh, 0, rs);
}

void drawLabels() {
  tft.setTextFont(1);
  tft.setTextSize(2);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  for (int r = 0; r < rs; r++) {
    tft.setCursor(24, r2y(r));
    tft.printf("%02d", r);
  }
  for (int c = 0; c < cs; c++) {
    tft.setCursor(c2x(c), 15);
    tft.print(DAYSTR[c]);
  }
}

void drawSlot(int r, int c) {
  uint32_t color = schedule[r][c] ? TFT_WHITE : TFT_BLACK;
  tft.fillRect(c2x(c) + 1, r2y(r) + 1, cw - 2, ch - 2, color);
}

void drawSchedule() {
  for (int r = 0; r < rs; r++) {
    for (int c = 0; c < cs; c++) {
      tft.drawRect(c2x(c), r2y(r), cw, ch, TFT_DARKGREY);
      drawSlot(r, c);
    }
  }
}

void selectSlot(int x, int y) {
  int c = x2c(x);
  int r = y2r(y);
  if (c < 0 || c >= cs || r < 0 || r >= rs) return;
  schedule[r][c] = !schedule[r][c];
  drawSlot(r, c);
}

void initWiFi() {
  WiFi.begin(SSID, PWD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(100);
  }
}

void initSNTP() {
  sntp_set_sync_interval(15 * 60 * 1000UL);  // 15 minutes
  esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
  esp_sntp_setservername(0, "pool.ntp.org");
  esp_sntp_init();
  setTimezone();
}

void setTimezone() {
  setenv("TZ", TIMEZONE, 1);
  tzset();
}

void drawTime() {
  static char buff[50];
  static struct tm t;
  getLocalTime(&t);

  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextDatum(MC_DATUM);
  tft.setTextSize(1);

  sprintf(buff, " %2d:%02d:%02d ", t.tm_hour, t.tm_min, t.tm_sec);
  tft.drawString(buff, tft.width() / 2, 130, 7);

  sprintf(buff, "  %s, %s %d, %d ",
          DAYSTR[t.tm_wday], MONTHSTR[t.tm_mon],
          t.tm_mday, t.tm_year + 1900);
  tft.drawString(buff, tft.width() / 2, 200, 4);
}

bool isOn() {
  static struct tm t;
  getLocalTime(&t);
  return schedule[t.tm_hour][t.tm_wday] && isAlarmOn;
}

void initDisplay() {
  tft.begin();
  tft.setRotation(0);
  tft.fillScreen(TFT_BLACK);
  tft.setTouch(cal);
}

void btnView_pressed(void) {
  if (btnView.justPressed()) {
    isTimeView = !btnView.getState();
    drawView();
    tft.setTextFont(4);
    tft.setTextSize(1);
    btnView.drawSmoothButton(isTimeView, 1, TFT_BLACK, isTimeView ? "alarm" : "time");
  }
}

void btnAlarm_pressed(void) {
  if (btnAlarm.justPressed()) {
    isAlarmOn = !btnAlarm.getState();
    tft.setTextFont(4);
    tft.setTextSize(1);
    btnAlarm.drawSmoothButton(isAlarmOn, 1, TFT_DARKGREY, isAlarmOn ? "on" : "off");
  }
}

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

  btnView.initButtonUL(x - w - 10, y, w, h, TFT_DARKGREY, TFT_DARKGREY, TFT_BLACK, "alarm", 1);
  btnView.setPressAction(btnView_pressed);
  btnView.drawSmoothButton(isTimeView, 1, TFT_BLACK);

  btnAlarm.initButtonUL(x + 10, y, w, h, TFT_DARKGREY, TFT_BLACK, TFT_DARKGREY, "off", 1);
  btnAlarm.setPressAction(btnAlarm_pressed);
  btnAlarm.drawSmoothButton(isAlarmOn, 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();
    }
  }
}

void drawView() {
  tft.fillScreen(TFT_BLACK);
  initButtons();
  if (isTimeView) {
    drawTime();
  } else {
    drawSchedule();
    drawLabels();
  }
}

void updateView() {
  static uint16_t x = 0, y = 0;
  if (isTimeView) {
    drawTime();
  } else {
    if (tft.getTouch(&x, &y)) {
      selectSlot(x, y);
    }
  }
}

void setup() {
  initWiFi();
  initSNTP();
  initDisplay();
  pinMode(alarmPin, OUTPUT);
  drawView();
}

void loop() {
  updateView();
  handleButtons();
  digitalWrite(alarmPin, isOn() ? HIGH : LOW);
  delay(100);
}

As you can see, this is quite a bit of code. The following section provides you with a quick overview of the functions and their meaning.

Functions Overview

  • drawLabels(): Draws labels for the rows and columns on the display.
  • drawSlot(int r, int c): Draws an hour slot on the schedule grid based on the provided row and column.
  • drawSchedule(): Draws the entire schedule grid on the display.
  • selectSlot(int x, int y): Allows users to select an hour slot on the schedule grid by touching the display.
  • initWiFi(): Initializes the WiFi connection using the provided SSID and password.
  • initSNTP(): Initializes the SNTP (Simple Network Time Protocol) for time synchronization.
  • setTimezone(): Sets the timezone for the device based on the provided constant.
  • drawTime(): Draws the current time and date on the display.
  • isOn(): Checks if the current time corresponds to a scheduled slot and the alarm is enabled.
  • initDisplay(): Initializes the display and sets up touch functionality.
  • btnView_pressed() and btnAlarm_pressed(): Handle button press events for switching between time view and schedule view, and toggling the alarm status.
  • initButtons(): Initializes the buttons on the display for interacting with the project.
  • handleButtons(): Handles button press and release actions on the display.
  • drawView(): Draws either the time view or the schedule view based on the current mode.
  • updateView(): Updates the display based on user interactions and current mode.

Let’s have a closer look at these functions and other parts of the code.

Library Inclusions

The code begins by including several libraries necessary for the functionality of the ESP32 and the display. We need libraries for the TFT display, the Wi-Fi connection, the time synchronization with an SNTP time server and the names of months and days (datestrsl.h)

#include "tft_setup.h"
#include "TFT_eSPI.h"
#include "TFT_eWidget.h"
#include "stdarg.h"
#include "WiFi.h"
#include "esp_sntp.h"
#include "datestrs.h"

Constants and Variables

Next, we define several constants and variables that will be used throughout the program. These include Wi-Fi credentials, display dimensions, alarm pin, and a schedule array.

const char* TIMEZONE = "AEST-10AEDT,M10.1.0,M4.1.0/3";
const char* SSID = "SSID";
const char* PWD = "PASSWORD";

const int rs = 24, cs = 7;
const int alarmPin = 25;

bool schedule[rs][cs];
bool isTimeView = true;
bool isAlarmOn = false;

You will have to set the SSID and PWD for your WiFi and you probably also will have to change the TIMEZONE. The above time zone specification “AEST-10AEDT,M10.1.0,M4.1.0/3” is for Australia, which corresponds to the Australian Eastern Standard Time (AEST) with daylight saving time adjustments.

The parts of this time zone definition are as follows

  • AEST: Australian Eastern Standard Time
  • -10: UTC offset of 10 hours ahead of Coordinated Universal Time (UTC)
  • AEDT: Australian Eastern Daylight Time
  • M10.1.0: Transition to daylight saving time occurs on the 1st Sunday of October
  • M4.1.0/3: Transition back to standard time occurs on the 1st Sunday of April, with a 3-hour difference from UTC.

For other time zone definitions have a look at the Posix Timezones Database. Just copy and paste the string you find there and change the TIMEZONE constant accordingly.

Coordinate Conversion Functions

The following functions convert between coordinate systems for the display:

int c2x(int c) { ... }
int r2y(int r) { ... }
int x2c(int x) { ... }
int y2r(int y) { ... }

c2x() and r2x() map columns and rows (c,r) in the schedule grid to screen coordinates (x,y), while x2c() and y2r() perform the reverse operation. They are need to convert a touch event into a booking of a time slot, for instance.

Drawing Functions

The code includes several functions to draw elements on the display:

drawLabels

This function draws the labels for the rows (hours) and columns (week days) of the schedule.

void drawLabels() {
  tft.setTextFont(1);
  tft.setTextSize(2);
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  for (int r = 0; r < rs; r++) {
    tft.setCursor(24, r2y(r));
    tft.printf("%02d", r);
  }
  for (int c = 0; c < cs; c++) {
    tft.setCursor(c2x(c), 15);
    tft.print(DAYSTR[c]);
  }
}

Hour and Day labels for Schedule
Hour and Day labels for Schedule

drawSlot

This function draws a single hour time slot in the schedule, filling it with white if the alarm is set for that time.

void drawSlot(int r, int c) {
  uint32_t color = schedule[r][c] ? TFT_WHITE : TFT_BLACK;
  tft.fillRect(c2x(c) + 1, r2y(r) + 1, cw - 2, ch - 2, color);
}
A hour time slot
A hour time slot

drawSchedule

This function draws the entire schedule grid, calling drawSlot() for each time slot.

void drawSchedule() {
  for (int r = 0; r < rs; r++) {
    for (int c = 0; c < cs; c++) {
      tft.drawRect(c2x(c), r2y(r), cw, ch, TFT_DARKGREY);
      drawSlot(r, c);
    }
  }
}
Hourly grid for Schedule
Hourly grid for Schedule

selectSlot

This function toggles the state and color of a time slot when the user touches it on the display.

void selectSlot(int x, int y) {
  int c = x2c(x);
  int r = y2r(y);
  if (c < 0 || c >= cs || r < 0 || r >= rs) return;
  schedule[r][c] = !schedule[r][c];
  drawSlot(r, c);
}

Wi-Fi and SNTP Initialization

The following functions handle the initialization of Wi-Fi and SNTP (Simple Network Time Protocol):

initWiFi

This function connects the ESP32 to the specified Wi-Fi network.

void initWiFi() {
  WiFi.begin(SSID, PWD);
  while (WiFi.status() != WL_CONNECTED) {
    delay(100);
  }
}

initSNTP

This function sets up the SNTP service to synchronize the time. It essentially connects to a server on the internet that provides very accurate time information and synchronizes the internal clock of the ESP32 accordingly.

void initSNTP() {
  sntp_set_sync_interval(15 * 60 * 1000UL);  // 15 minutes
  esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL);
  esp_sntp_setservername(0, "pool.ntp.org");
  esp_sntp_init();
  setTimezone();
}

As long as there is a Wi-Fi and internet connection, this ensures that the clock of our digital timer switch is always accurate and automatically adjusts to daylight savings time. For more details have a look at the How to synchronize ESP32 clock with SNTP server tutorial.

setTimezone

This function sets the timezone for the application. You have to make sure that this functions runs every time the ESP32 starts, otherwise your clock will be off.

void setTimezone() {
  setenv("TZ", TIMEZONE, 1);
  tzset();
}

Time Display Function

The drawTime() function retrieves the current local time, which is synchronized via SNTP, and displays the time and date on the screen.

void drawTime() {
  static char buff[50];
  static struct tm t;
  getLocalTime(&t);

  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextDatum(MC_DATUM);
  tft.setTextSize(1);

  sprintf(buff, " %2d:%02d:%02d ", t.tm_hour, t.tm_min, t.tm_sec);
  tft.drawString(buff, tft.width() / 2, 130, 7);

  sprintf(buff, "  %s, %s %d, %d ",
          DAYSTR[t.tm_wday], MONTHSTR[t.tm_mon],
          t.tm_mday, t.tm_year + 1900);
  tft.drawString(buff, tft.width() / 2, 200, 4);
}

The function uses the constants for the names of days and months stored in the datestrs.h file.


Time and Date shown on Display
Time and Date shown on Display

If you need more details have a look at the Digital Clock with CrowPanel 3.5″ ESP32 Display and the Digital Clock on e-Paper Display tutorials. where we also implement a digital clock.

Alarm Check Function

The isOn() function checks if the alarm should be triggered based on the current time and schedule. If the grid cell schedule[t.tm_hour][t.tm_wday] contains true and the overall alarm switch is on (isAlarmOn), the function returns true and otherwise false.

bool isOn() {
  static struct tm t;
  getLocalTime(&t);
  return schedule[t.tm_hour][t.tm_wday] && isAlarmOn;
}

Display Initialization

The initDisplay() function initializes the display by setting the screen rotation, filling the screen with black, setting the calibration parameter for the touch screen.

void initDisplay() {
  tft.begin();
  tft.setRotation(0);
  tft.fillScreen(TFT_BLACK);
  tft.setTouch(cal);
}

Button Functions

The code includes functions to handle button presses for toggling between time view and alarm settings:

btnView_pressed

This function toggles the view between the current time and the alarm schedule.

void btnView_pressed(void) {
  if (btnView.justPressed()) {
    isTimeView = !btnView.getState();
    drawView();
    tft.setTextFont(4);
    tft.setTextSize(1);
    btnView.drawSmoothButton(isTimeView, 1, TFT_BLACK, isTimeView ? "alarm" : "time");
  }
}
Toggling time and schedule view
Toggling time and schedule view

btnAlarm_pressed

This function toggles the overall alarm switch on and off. If the switch is off the alarm schedule is disabled.

void btnAlarm_pressed(void) {
  if (btnAlarm.justPressed()) {
    isAlarmOn = !btnAlarm.getState();
    tft.setTextFont(4);
    tft.setTextSize(1);
    btnAlarm.drawSmoothButton(isAlarmOn, 1, TFT_DARKGREY, isAlarmOn ? "on" : "off");
  }
}
Toggling of alarm switch

Button Initialization

The initButtons() function initializes the buttons on the display.

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

  btnView.initButtonUL(x - w - 10, y, w, h, TFT_DARKGREY, TFT_DARKGREY, TFT_BLACK, "alarm", 1);
  btnView.setPressAction(btnView_pressed);
  btnView.drawSmoothButton(isTimeView, 1, TFT_BLACK);

  btnAlarm.initButtonUL(x + 10, y, w, h, TFT_DARKGREY, TFT_BLACK, TFT_DARKGREY, "off", 1);
  btnAlarm.setPressAction(btnAlarm_pressed);
  btnAlarm.drawSmoothButton(isAlarmOn, 1, TFT_BLACK);
}
Buttons for View and Alarm
Buttons for View and Alarm

Button Handling

The handleButtons() function checks for button presses and executes the corresponding actions.

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

View Drawing Function

The drawView() function updates the display based on the current view mode (time or schedule).

void drawView() {
  tft.fillScreen(TFT_BLACK);
  initButtons();
  if (isTimeView) {
    drawTime();
  } else {
    drawSchedule();
    drawLabels();
  }
}

View Update Function

The updateView() function refreshes the display based on user interaction.

void updateView() {
  static uint16_t x = 0, y = 0;
  if (isTimeView) {
    drawTime();
  } else {
    if (tft.getTouch(&x, &y)) {
      selectSlot(x, y);
    }
  }
}

Setup Function

The setup() function initializes the Wi-Fi, SNTP, display, and switches GPIO25 into output mode. We will connect an LED or an Relay to this pin and control them via the alarm schedule.

void setup() {
  initWiFi();
  initSNTP();
  initDisplay();
  pinMode(alarmPin, OUTPUT);
  drawView();
}

Loop Function

Finally, the loop() function continuously updates the view, handles button presses, and controls the alarm pin based on the schedule.

void loop() {
  updateView();
  handleButtons();
  digitalWrite(alarmPin, isOn() ? HIGH : LOW);
  delay(100);
}

And this is the complete code for a digital timer switch. In the next two section, I show you how to connect an LED or a Relay to the Crowpanel Display, which we then can control via the alarm schedule.

Connecting an LED to the Digital Timer Switch

To test that the code for our digital timer switch works, we connect an led to GPIO25 as follows:

LED connected to CrowPanel 3.5" ESP32 Display
LED connected to CrowPanel 3.5″ ESP32 Display

Make sure that the short pin (cathode) of the LED is connected to ground (black wire) and the long pin via resistor to GPIO25 (yellow wire). The Crowpanel comes with a color coded connector, so if you follow the colors you can’t go wrong. The following photo shows the actual wiring with the color coded cables:

Wiring of LED with Crowpanel Display
Wiring of LED with Crowpanel Display

To test the digital timer switch read the current time and date, e.g. 12:37:00 on Wednesday and then set the corresponding time slot the alarm schedule:

Schedule set for 12:00-13:00 on Wednesday
Schedule set for 12:00-13:00 on Wednesday

If the overall alarm button is on, the LED should be on. You then can switch it off by toggling the overall alarm button or changing the schedule.

Connecting a Relay to the Digital Timer Switch

If you want to control lights in your home or the pump of you sprinkler you will need a relay. The following diagram shows you how to wire that up:

Relay connected to CrowPanel 3.5″ ESP32 Display

VCC (red cable) and GND (black cable) are connected to the corresponding pins on a relay module. GPIO25 is connected to the input pin (yellow cable) of the relay.

The high voltage, high current device you want to control is connected to the COM and NC (normal open) terminals of the relay module. Be very careful when operating with voltages higher than 50V ! You must make sure that the relay module is rated for the voltage and current you want to switch. Typical relay modules (AITRIP, HiLetgo) can switch 220V at 4A up to 10A.

The photo below shows the wiring of the relay module with the CrowPanel Display:

Wiring of Relay with Crowpanel Display
Wiring of Relay with CrowPanel Display

Conclusion

In this tutorial you learned how to build a Digital Timer Switch with the CrowPanel 3.5″ ESP32 Display. Compared to many commercially available products or Digital Timer Switch has always accurate time and due the large display makes it very easy to quickly set and change schedules.

There are many possible extensions to this project. You may want to be able to set schedules with a resolution of minutes or seconds. Or may be you want to program a schedule for an entire month or year.

Instead of connecting a relay you could also use Bluetooth, MQTT or other protocols to control devices wirelessly. The code above gives you a framework for it.

If you have any questions feel free to leave them in the comment section.

Happy tinkering 😉

Links

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