Skip to Content

Reloj digital con pantalla CrowPanel 3.5″ ESP32

Reloj digital con pantalla CrowPanel 3.5″ ESP32

En este tutorial aprenderás cómo construir un Reloj Digital siempre preciso con la pantalla CrowPanel 3.5″ ESP32. Vamos a sincronizar nuestro reloj por WiFi con un proveedor de hora en internet y usaremos la librería TFT_eSPI para crear una interfaz atractiva en la pantalla.

¡Vamos a empezar!

Componentes necesarios

Para este proyecto solo necesitarás la pantalla CrowPanel 3.5″ ESP32 de ELECROW y el Arduino IDE. El panel viene con un cable USB y un cable DuPont de 4 pines, así que no necesitarás cables adicionales.

Pantalla CrowPanel 3.5″ ESP32

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.

Características de la pantalla CrowPanel 3.5″ ESP32

El CrowPanel 3.5″ ESP32 Display de ELECROW es una pantalla táctil resistiva con una resolución TFT de 480*320 y un ESP32-WROVER-B como procesador de control. Puedes conseguir la pantalla con una carcasa acrílica (ver foto abajo), lo que significa que no tendrás que fabricar una carcasa tú mismo.

CrowPanel 3.5" ESP32 Display with Housing
Pantalla CrowPanel 3.5″ ESP32 con carcasa (source)

Además, la placa viene con una ranura para tarjeta TF, una interfaz UART, una interfaz I2C, una interfaz para altavoz, un conector de batería con capacidad de carga y un pequeño puerto GPIO con dos pines GPIO. Consulta el pinout a continuación:

Pinout of CrowPanel 3.5" ESP32 Display
Pinout de la pantalla CrowPanel 3.5″ ESP32

La siguiente tabla muestra qué pines GPIO están asignados a cada una de las tres interfaces IO.

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

La placa se puede alimentar a través del puerto USB (5V, 2A) o conectando una batería LiPo estándar de 3.7V al conector BAT. Solo asegúrate de la polaridad del conector y la batería. Si el cable USB y la batería están conectados al mismo tiempo, la placa cargará la batería (la corriente máxima de carga es 500mA).

Puedes programar la placa usando varios entornos de desarrollo como Arduino IDE, Espressif IDF, Lua RTOS y Micro Python, y es compatible con la LVGL librería gráfica. Sin embargo, en este tutorial usaremos el Arduino IDE y la TFT_eSPI librería gráfica para crear la interfaz de usuario de nuestro reloj.

Serie de pantallas CrowPanel ESP32

Ten en cuenta que la pantalla CrowPanel 3.5″ ESP32 forma parte de una familia de pantallas que van desde 2.4 pulgadas hasta 7 pulgadas. Además de la diferencia de tamaño, también varían en resolución, controlador de pantalla y modelo de ESP32. Consulta la tabla a continuación.

Pantallas CrowPanel ESP32 (source)

En este tutorial, usaremos 3.5″ display. Pero los ejemplos de código y el procedimiento de configuración serían muy similares para la 2.4″ display y la 2.8» display, ya que utilizan el mismo o un controlador de pantalla similar (ILI9341, ILI9488). Consulta las secciones marcadas en amarillo en la tabla anterior.

Para las pantallas más grandes (4.3″, 5″, 7″), sin embargo, probablemente los ejemplos de código no funcionen ya que la librería TFT_eSPI no parece soportar sus controladores (NV3047, IL6122, EK9716BD3). Sin embargo, no lo he probado personalmente. Si lo pruebas, siéntete libre de comentarlo.

Crear la estructura del proyecto

Antes de entrar en detalles, vamos a crear la estructura del proyecto y la configuración para la librería TFT_eSPI.

Abre tu Arduino IDE y crea un proyecto llamado «digiclock» y guárdalo (Guardar como …). Esto creará una carpeta «digiclock» con el archivo «digiclock.ino» dentro. En esta carpeta crea otro archivo llamado «tft_setup.h«. Tu carpeta del proyecto debería verse así:

Project Folder Structure
Estructura de la carpeta del proyecto

Y en tu Arduino IDE ahora deberías tener dos pestañas llamadas «digiclock.ino» y «tft_setup.h«.

Arduino IDE with two Tabs
Arduino IDE con dos pestañas

Haz clic en la pestaña «tft_setup.h» para abrir el archivo y copia el siguiente código en él:

#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

Este código le indica a la librería TFT_eSPI qué pantalla estamos usando. Específicamente, le decimos a la librería las dimensiones de la pantalla (TFT_WIDTH, TFT_WIDTH), su controlador (ILI9488_DRIVER), qué pines SPI se usan para controlarla y qué fuentes cargar. Sin estos ajustes, no podrás mostrar nada en la pantalla.

Para más información detallada, echa un vistazo al tutorial CrowPanel 2.8″ ESP32 Display : Easy Setup Guide. Allí explicamos cómo configurar la pantalla de 2.8″. Pero los pasos y descripciones aplican igual para la pantalla de 3.5″. Solo cambian los ajustes de dimensiones y controlador.

Calibrar la pantalla táctil

La pantalla CrowPanel 3.5″ viene con una pantalla táctil resistiva que necesitas calibrar primero, antes de poder usarla con la librería TFT_eSPI. Copia el código de abajo en el archivo «digiclock.ino«, compílalo y súbelo a la pantalla CrowPanel 3.5».

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

Cuando el código esté en ejecución, la pantalla mostrará una flecha y te pedirá que toques la esquina a la que apunta. Esto se repetirá para las otras tres esquinas. Usa el pequeño lápiz que viene con la pantalla para hacerlo e intenta tocar las esquinas lo más preciso posible.

Calibration of touch screen
Calibración de la pantalla táctil

Al final del proceso de calibración, el código imprime los 5 parámetros de calibración (coordenadas de las esquinas y orientación de la pantalla) en el Monitor Serie. Deberías ver algo como esto:

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

Copia los parámetros en algún sitio, ya que los necesitarás en el código del reloj. La calibración se repite cada 10 segundos, así que puedes intentarlo varias veces para obtener los parámetros más precisos.

Para más información sobre el proceso de calibración, consulta el tutorial CrowPanel 2.8″ ESP32 Display : Easy Setup Guide. Ten en cuenta, sin embargo, que allí la calibración se hace para la pantalla en orientación vertical, mientras que aquí usamos la pantalla en modo horizontal (ver el setRotation(1) en el código de calibración anterior).

A continuación, veamos cómo obtener la hora de internet.

Descargar datos de hora desde Internet

Me gustan los relojes que se ajustan automáticamente al horario de verano y que siempre son precisos, así no tengo que ponerlos en hora. Si tienes Wi-Fi e internet, la forma más fácil de lograr esto es descargar regularmente la hora actual de un servidor de tiempo y luego ajustar el reloj interno del ESP32. Para más información sobre esto, consulta el tutorial sobre cómo construir un Automatic Daylight Savings Time Clock.

Aquí vamos a usar el mismo método que en ese tutorial. Es decir, usaremos la WorldTimeAPI para obtener nuestros datos de hora. WorldTimeAPI es un servicio web sencillo, que devuelve la hora actual en texto plano o JSON. Puedes usar su web para obtener la hora de una zona horaria concreta o según la IP de tu ordenador. Usaremos esta última opción. Es más fácil y así nuestro reloj se ajustará automáticamente no solo al horario de verano, sino también al cambiar de zona horaria.

Es muy fácil probar WorldTimeAPI. Solo haz clic en este enlace: http://worldtimeapi.org/api/ip o introdúcelo en la barra de búsqueda de tu navegador. No necesitas indicar tu dirección IP explícitamente. El servicio web la detecta automáticamente según el origen de la consulta.

En tu navegador deberías ver una salida similar a la siguiente (he añadido algo de formato para que sea más legible y he ocultado mi IP)

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

Esta salida está en formato JSON. Contiene más información además de la hora actual. Pero nos interesa especialmente el campo datetime, que nos da la hora local actual. En este ejemplo es "2023-11-16T12:09:46.409360+11:00".

Cada vez que visites este enlace, obtendrás la hora actualizada. Solo asegúrate de no consultarlo demasiado seguido, ¡si no podrías ser bloqueado!

Con esto ya tenemos todo lo necesario para implementar nuestro Reloj Digital.

Implementación de un Reloj Digital

En esta sección vamos a implementar el Reloj Digital. Se verá así:

Example Display of the Digital Clock
Ejemplo de pantalla del Reloj Digital

Muestra la hora actual (sincronizada por internet), día, mes y año, y dos botones. Con el botón «24h» podemos cambiar entre formato 24h y 12h. Y el botón «day» nos permite cambiar el brillo de la pantalla para noche y día.

Para implementar este reloj, empecemos por la estructura del proyecto.

Estructura de la carpeta del proyecto

En el paso anterior, ya creaste la carpeta del proyecto «digiclock» con los archivos «digiclock.ino» y «tft_setup.h» dentro. Ahora añade otro archivo llamado «datestr.h«. Tu carpeta del proyecto debería verse así:

Project Folder Structure
Estructura de la carpeta del proyecto

y tu Arduino IDE ahora debería tener tres pestañas: «digiclock.ino«, «datestr.h» y «tft_setup.h«:

Tabs in Arduino IDE
Pestañas en Arduino IDE

El archivo «tft_setup.h» ya está relleno con el código correcto. Pero los otros dos archivos («digiclock.ino«, «datestr.h«) hay que actualizarlos. Empecemos por el archivo «datestr.h«.

Nombres de días y meses

Además de la hora, también queremos mostrar la fecha actual como texto en la pantalla. Por ejemplo, Thu, May 30, 2024. Sin embargo, de WorldTimeAPI no obtenemos los nombres del mes o del día actual. Tenemos que convertir un número de mes, como 5, en un nombre de mes, por ejemplo «May».

La librería TimeLib, que usaremos más adelante, en realidad tiene funciones para esto pero no conseguí que funcionaran. Hay un problema de asignación de memoria al escribir en una cadena mediante sprintf, que no quise perder tiempo en resolver.

Por eso definimos nuestros propios nombres de meses y días. Es muy sencillo y tiene la ventaja de que puedes cambiar los nombres a tu gusto. Nombres completos (Monday), tres letras (Mon), dos letras (MO), mayúsculas o minúsculas, otro idioma… lo que prefieras. Solo copia el siguiente código en el archivo «datestr.h» y cámbialo si quieres.

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

Programa principal

En esta sección vamos a implementar el programa principal, el núcleo de nuestro reloj. Copia el siguiente código en el archivo «digiclock.ino«. Sustituye completamente el código de calibración que había antes.

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

Ahora es bastante código. En las siguientes secciones lo desglosamos y explicamos cómo funcionan todas las partes juntas.

La mayor parte del código se basa en estos tres tutoriales: Cómo construir un Automatic Daylight Savings Time Clock, cómo construir un LED Ring Clock with WS2812, y el CrowPanel 2.8″ ESP32 Display : Easy Setup Guide. Si tienes dificultades para entender alguna parte del código o las explicaciones, échales un vistazo.

Librerías

Primero incluimos las librerías necesarias. Además de la librería «TFT_eSPI«, necesitarás install la «TFT_eWidget«, la «ArduinoJson» y la «TimeLib«. Usa el gestor de librerías del Arduino IDE como siempre.

Las otras librerías: «stdarg», «WiFi» y «HTTPClient» forman parte del core de ESP32 y no necesitas instalarlas por separado. Y «tft_setup.h» y «datestrs.h» son los archivos del proyecto Arduino digiclock que creamos antes. Ya existen pero deben incluirse como se muestra.

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

Estas librerías proporcionan la funcionalidad necesaria para la conectividad WiFi, hacer peticiones HTTP, analizar JSON, gestionar la hora y crear la interfaz de usuario de nuestro reloj.

Constantes y objetos

A continuación, definimos algunas constantes y objetos. El StaticJsonDocument captura la respuesta de la WebTime API a nuestra petición de hora. cal contiene los parámetros de calibración de la pantalla. Y timeStr y dateStr son buffers de caracteres que usaremos para formatear la hora y la fecha mostradas.

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 es el objeto para controlar la pantalla y btn1 y btn2 son los dos objetos botón que aparecen en la pantalla. También guardamos los objetos botón en un array btns para simplificar la gestión de eventos de los botones. Y is24h y isDay son dos banderas booleanas que representan el estado de los dos botones.

Funciones de los botones

Las funciones btn1_pressed() y btn2_pressed() se llaman cuando se toca el botón correspondiente en la pantalla táctil. Establecen las banderas de estado de los botones is24h y isDay y cambian la apariencia del botón.

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

Inicialización de los botones

La función initButtons() crea la apariencia inicial y define la ubicación de los dos botones en la pantalla.

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

Si se muestran completamente, los botones aparecerían como rectángulos redondeados como en la imagen de abajo

Original shape of Buttons
Forma original de los botones

Sin embargo, he colocado los botones de forma que queden a medio salir de la pantalla, lo que hace que parezcan pestañas:

Partially visible Buttons
Botones parcialmente visibles

Creo que así quedan mejor, pero si no te gusta solo cambia la coordenada y a y = tft.height() - h - 10 para mostrar el botón completo.

También ten en cuenta que tenemos que llamar a tft.setTextFont(4), ya que la función initButtonUL() no tiene un parámetro para establecer la fuente.

Gestión de eventos de los botones

La función handleButtons() gestiona los eventos de los botones. Itera por todos los botones guardados en el array btns. Si se detecta un evento táctil mediante getTouch() y las coordenadas x,y del evento están dentro del área del botón (btns[b]->contains(x, y)), se llama a la función correspondiente del botón.

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

Comprobar la sincronización de la hora

Queremos sincronizar el temporizador interno del ESP32 con la hora de internet que descargamos de WorldTimeAPI. Sin embargo, no queremos sincronizar demasiado a menudo, ¡si no WorldTimeAPI nos bloqueará!

La función shouldSyncTime() comprueba si el minuto actual es cero y 3 segundos o si el año actual es 1970 y entonces devuelve true, indicando que debemos sincronizar la hora. Eso significa que sincronizamos cada hora y lo hacemos 3 segundos después de la hora en punto para captar el cambio de horario de verano que puede ocurrir en ese momento. Así que, en el peor de los casos, nuestro reloj tendrá un desfase de 3 segundos.

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

También comprobamos si el año actual es 1970. Este es el año que tu reloj ESP32 mostrará al arrancar (inicio de Unix time). Esto significa que, cuando el ESP32 se enciende, lo primero que haremos será sincronizar la hora, aunque no sea en punto. ¡Sin esto, la hora podría estar completamente mal hasta una hora!

Sincronizar la hora

La función syncTime() descarga la hora actual de WorldTimeAPI y ajusta el reloj interno del ESP32 en consecuencia. Para ello, hace una GET request a la URL especificada y analiza la respuesta como JSON usando la librería ArduinoJson.

Del JSON extrae el año, mes, día, hora, minuto, segundo e información de zona horaria y ajusta la hora interna del ESP32 usando la función setTime() de la librería TimeLib.

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

Actualizar la pantalla

La función updateDisplay() primero lee la hora interna actual mediante now() y, en base a eso, rellena los buffers timeStr y dateStr con una representación en texto de la hora y la fecha. Dependiendo del estado is24h del botón de formato de hora, la hora se muestra en formato 24h o 12h.

Fíjate en los espacios extra en las cadenas de formato, por ejemplo " %s, %s %d, %d ". Son necesarios porque las cadenas de hora y fecha tienen distinta longitud y la función updateDisplay() simplemente sobrescribe la hora mostrada en vez de limpiar la pantalla. Así se evita el parpadeo, pero necesitas los espacios.

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

Una vez que tenemos la hora y la fecha en los buffers de texto, simplemente las mostramos en pantalla llamando a drawString() con la cadena y las coordenadas. setTextDatum(MC_DATUM) centra el texto, y setTextColor() usa color blanco o gris oscuro, según el estado isDay del botón de día/noche.

La función updateDisplay() básicamente mostrará cuatro pantallas diferentes, según el estado de las banderas is24h y isDay. La imagen de abajo muestra estas cuatro pantallas:

The four different Screens of the Clock
Las cuatro pantallas diferentes del reloj

Por la cámara y la luz, las pantallas parecen azuladas, pero en realidad los colores son negro, blanco y gris. Además, la diferencia entre modo noche y día es más notable en persona.

Ten en cuenta que la función updateDisplay() no funcionará para otros tamaños de pantalla sin algunos cambios. Tendrás que ajustar el tamaño de la fuente usando otra fuente o tamaño. También la posición vertical de las cadenas de hora y fecha está codificada y habría que ajustarla.

Función de configuración

En la función setup(), primero establecemos la conexión Wi-Fi y luego inicializamos la pantalla TFT. Es importante calibrar la pantalla táctil mediante setTouch(cal), donde usamos los parámetros de calibración. Sin eso, los botones no funcionarán correctamente. También ten en cuenta que ponemos la pantalla en modo horizontal con 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();
}

Yo uso un fondo negro, pero puedes elegir otro color. Sin embargo, también tendrás que ajustar el color de fondo de las cadenas y botones en updateDisplay() y initButtons().

La última parte de la configuración es la llamada a initButtons(), que crea y dibuja los botones.

Función loop

Con todas las funciones auxiliares anteriores, el bucle principal es ahora muy sencillo. Primero comprobamos si debemos sincronizar el ESP32 con la hora de internet. Si es así, llamamos a syncTime() para hacerlo.

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

Después simplemente actualizamos la pantalla con la hora actual y luego llamamos a handleButtons() para gestionar los eventos de los botones. El bucle se ejecuta cada 50 mseg, lo cual es suficientemente rápido para responder a los botones. Las otras funciones podrían ejecutarse a menor frecuencia, por ejemplo cada 100 a 500 mseg, pero la mayor velocidad no causa problemas.

¡Y eso es todo! ¡Un reloj siempre preciso y muy chulo!

Conclusiones

El Reloj Digital que hemos implementado funciona genial, pero aquí tienes algunas ideas para mejorarlo aún más.

Por ejemplo, en vez de cambiar el brillo manualmente, podríamos añadir una fotorresistencia (LDR) para hacerlo automáticamente según la luz ambiente. Además, no tiene mucho sentido mostrar la hora si no hay nadie para verla. Con un sensor PIR podríamos detectar movimiento y activar el reloj si se detecta una persona. Consulta el tutorial LED Ring Clock with WS2812 para más información.

Quizá quieras añadir otros botones para cambiar el formato de fecha o para controlar luces externas. Echa un vistazo al CrowPanel 2.8″ ESP32 Display : Easy Setup Guide , donde controlamos dos LEDs desde la pantalla. WorldTimeAPI también permite especificar zonas horarias, así que podrías tener botones para cambiar entre zonas o ciudades.

Como alternativa a WorldTimeAPI, también podrías conectarte a un servidor SNTP, lo que te permitiría sincronizar la hora con más frecuencia. Consulta el tutorial How to synchronize ESP32 clock with SNTP server.

Como el CrowPanel tiene una interfaz I2C, podrías añadir fácilmente un sensor de temperatura, humedad o calidad del aire para mostrar más información en la pantalla. Consulta los tutoriales AM2320 digital temperature and humidity sensor Arduino tutorial y Interfacing Arduino and SGP30 Versatile Air Quality Sensor para más detalles sobre esto.

Por último, además de la hora, también puedes descargar datos meteorológicos actuales de internet y mostrarlos. Echa un vistazo a OpenWeather. Tienen un plan gratuito con suficiente cuota para actualizaciones horarias del tiempo.

Si tienes más ideas o preguntas, no dudes en dejar un comentario. ¡Y si no, diviértete ; )

Enlaces

Aquí tienes algunos enlaces que me resultaron útiles para escribir este post.