En este tutorial aprenderás cómo construir un reloj con anillo de LEDs usando una tira WS2812 y un ESP32. Añadiremos un sistema de atenuación automática y utilizaremos un proveedor de hora por internet para que el reloj siempre sea preciso. Por último, el reloj se activará por movimiento usando un sensor PIR, para no gastar energía innecesariamente.
El siguiente vídeo corto muestra el reloj en funcionamiento. Puedes ver las marcas de las horas (puntos blancos) y los segundos avanzando (punto blanco en movimiento).

La hora actual está marcada por el punto naranja y los minutos por los puntos amarillos. Así que, en la imagen de arriba, el reloj marca las 11:36.
Al sincronizar el reloj por Wi-Fi con un proveedor de hora en internet, nos aseguramos de que siempre muestre la hora correcta, sin importar los cambios de horario de verano, cortes de energía o la imprecisión del reloj interno.
Vamos a empezar con los componentes necesarios.
Componentes necesarios
Aquí tienes los componentes necesarios para el proyecto. En vez de la placa de desarrollo ESP32-C3 Mini que aparece abajo, yo usé una placa muy similar llamada ESP32-C3 SuperMini de AliExpress. La mía solo tenía un LED integrado de un solo color, mientras que la placa de abajo tiene un LED RGB. Pero aparte de eso, deberían ser casi idénticas y ambas deberían funcionar. Cualquier otro ESP32 o ESP8266 también sirve. Sin embargo, si quieres usar un Arduino, necesitarás un Wi-Fi shield.

ESP32-C3 Mini

Cable USB C

Anillo LED RGB

Set de cables Dupont

Breadboard

Kit de resistencias & LEDs

Set de resistencias variables

Sensor de movimiento

Set de fotorresistencias
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.
Base de la tira LED RGB WS2812
Vamos a usar un anillo de LEDs RGB WS2812 para nuestro reloj. El WS2812 es un tipo específico de LED RGB basado en el tamaño de encapsulado 5050. El LED RGB 5050 se refiere a las dimensiones del encapsulado, que son 5.0mm x 5.0mm. La imagen de abajo muestra un LED 5050 con su IC de control y los tres LEDs (verde, rojo, azul).

Ten en cuenta que existen otros controladores de tiras LED RGB similares como el WS2811, SK6812 y WS2815. Este quick guide contiene una buena comparación de los diferentes tipos. Aquí vamos a usar el WS2812.
El WS2812 funciona a 5V, tiene un controlador PWM integrado para mezclar colores y se puede encadenar usando una sola línea de comunicación para crear tiras de LEDs más largas. Lo más común es encontrar el WS2812 en tiras flexibles de LEDs RGB como la que se muestra abajo.

Estas tiras de LEDs vienen con diferentes cantidades y densidades de LEDs, y se pueden cortar a la longitud que necesites. Además de las tiras flexibles, también puedes encontrarlas en forma de anillos rígidos:

Los anillos pequeños vienen en una sola pieza, pero el anillo grande de 60 LEDs que vamos a usar para nuestro reloj viene en cuatro partes que tendrás que soldar juntas. La imagen de abajo muestra la parte trasera de las cuatro piezas:

Cómo ensamblar los cuatro segmentos en un solo anillo grande es el tema de la siguiente sección.
Construcción del reloj con anillo de LEDs
Como una hora tiene 60 minutos y un minuto 60 segundos, queremos usar un anillo con 60 LEDs. Como mencioné antes, por su tamaño viene en 4 secciones (cada una con 15 LEDs) que debemos soldar entre sí.
Ten en cuenta que las tiras y anillos LED RGB WS2812 tienen un lado de entrada (DIN) y uno de salida (DO), como se muestra en la imagen de abajo:

Empieza soldando el cable de señal (amarillo) al pad DIN de uno de los segmentos del anillo LED. Luego suelda un cable de tierra (negro) al pad GND y un cable de alimentación (rojo) al pad de 5V. Debería verse así:

Por último, tenemos que conectar los cuatro segmentos. Conecta GND con GND, 5V con 5V y DOUT con DIN en cada uno de los segmentos. Una conexión entre segmentos debería verse así.

Básicamente, no puedes conectar mal los pads, de lo contrario no obtendrás un anillo bonito:

Ten en cuenta que las tiras LED largas pueden sufrir caída de voltaje, lo que hace que los LEDs al final de la tira sean menos brillantes que los del principio. A veces por eso se encuentran tiras LED con alimentación en ambos extremos.
Con el anillo de 60 LEDs, no noté ninguna diferencia de brillo entre el primer y el último LED. Sin embargo, si lo notas, puedes conectar los pads GND y 5V entre el último y el primer segmento del anillo. En mi caso no fue necesario.
También puede que la soldadura te resulte un poco difícil porque los pads son muy pequeños. Ayuda colocar los segmentos del anillo LED en un soporte para fijarlos en su sitio. Yo usé una impresora 3D para crear el soporte o marco del reloj que se muestra en la siguiente sección.
Marco del reloj LED
El marco del reloj LED consta de tres partes. La trasera, donde va el anillo LED, un frontal transparente y una base que permite mantener el anillo LED de pie.

El marco completo del reloj LED se ve así y puedes encontrar el STL files here :

Lo siguiente es conectar el anillo LED al ESP32.
Cableado del reloj con anillo de LEDs
Conectar el anillo LED WS2812 al ESP32 es sencillo. Primero conecta el GND del ESP32 al GND del anillo LED (cable azul). Luego conecta el 5V del ESP32 a la entrada de 5V del anillo LED WS2812. Y finalmente conecta el GPIO3 del ESP32, a través de una resistencia de 220Ω, al DIN del WS2812.

Puedes probar sin la resistencia de 220Ω para hacer pruebas, pero es más seguro tenerla. Protege el GPIO del ESP32 de sobrecorrientes. En vez de GPIO3 puedes usar cualquier otro pin GPIO siempre que soporte PWM.
¡Ten en cuenta que el anillo LED puede consumir mucha corriente! Un solo LED RGB está compuesto por tres LEDs (rojo, verde, azul) y cada uno consume hasta 20mA cuando está al máximo. Eso significa que si un solo LED RGB está completamente encendido (luz blanca) consume hasta 60mA (yo medí 45mA). Multiplica 60mA por 60 LEDs y obtenemos una corriente de 3.6A , ¡si todos los LEDs del anillo están encendidos al máximo!
Eso normalmente es demasiada corriente para sacarla del ESP32 o del puerto USB. El código de prueba en la siguiente sección por eso ajusta el brillo del anillo LED a un valor bajo de 10, que es más que suficiente para hacer pruebas.
Por cierto, si necesitas más información sobre la placa SuperMini que estoy usando aquí, échale un vistazo al ESP32-C3 SuperMini Board tutorial.
Código de prueba para el anillo LED
Antes de intentar implementar algo complejo, es mejor probar el funcionamiento del anillo LED con un código sencillo primero. El código de abajo enciende secuencialmente los 60 LEDs y, cuando todos están encendidos, los apaga de nuevo. Esto nos permite comprobar que todos los LEDs funcionan y que los segmentos del anillo están correctamente cableados.
#include "Adafruit_NeoPixel.h"
#define DINPIN 3
#define NUMPIXELS 60
Adafruit_NeoPixel pixels(NUMPIXELS, DINPIN, NEO_GRB + NEO_KHZ800);
void setup() {
pixels.begin();
pixels.clear();
pixels.setBrightness(10);
pixels.show();
}
void loop() {
for (int i = 0; i < NUMPIXELS; i++) { // Switch all LEDs on
pixels.setPixelColor(i, pixels.Color(255, 255, 255)); // White
pixels.show();
delay(200);
}
for (int i = 0; i < NUMPIXELS; i++) { // Switch all LEDs off
pixels.setPixelColor(i, pixels.Color(0, 0, 0));
pixels.show();
delay(200);
}
}
En el fragmento de código anterior, estamos usando la Adafruit NeoPixel library . Si aún no la tienes, tendrás que install the library primero, antes de poder usar este código.
Vamos a desglosar el código de prueba para entender su funcionamiento en más detalle.
Constantes y variables
Primero definimos la constante DINPIN que especifica el pin de entrada de datos al que está conectado el anillo LED, y NUMPIXELS que define el número total de píxeles/LEDs en la tira. En nuestro caso tenemos 60 LEDs y usamos el GPIO 3.
#define DINPIN 3 #define NUMPIXELS 60
Función setup
En la función setup() , inicializamos la tira del anillo LED llamando a pixels.begin() , limpiamos cualquier color de píxel existente con pixels.clear() , ajustamos el nivel de brillo a 10 con pixels.setBrightness(10) , y finalmente mostramos el estado de los píxeles usando pixels.show() .
void setup() {
pixels.begin();
pixels.clear();
pixels.setBrightness(10);
pixels.show();
}
Como mencioné antes, ¡ten cuidado al cambiar el brillo! A brillo máximo, el anillo LED puede consumir demasiada corriente para tu ESP32, puerto USB o la fuente de alimentación que uses.
Función loop
La función loop() contiene un bucle que primero enciende todos los LEDs en blanco iterando por cada píxel usando un bucle for. La función pixels.setPixelColor() se usa para poner cada píxel en blanco (255, 255, 255) y luego se llama a pixels.show() para mostrar el estado de los LEDs. Se añade un retardo de 200ms entre cada actualización de píxel.
Después de poner todos los píxeles en blanco, otro bucle apaga todos los LEDs poniéndolos en negro (0, 0, 0) y actualizando la tira con pixels.show() de nuevo con un retardo de 200ms entre cada actualización.
void loop() {
for (int i = 0; i < NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.Color(255, 255, 255)); // White
pixels.show();
delay(200);
}
for (int i = 0; i < NUMPIXELS; i++) {
pixels.setPixelColor(i, pixels.Color(0, 0, 0)); // Black
pixels.show();
delay(200);
}
}
Si eso funciona, ¡enhorabuena! En la siguiente sección probamos algo un poco más complejo simulando la hora para mostrarla en el anillo LED y también mejoramos un poco nuestro circuito.
Mejorando el cableado del reloj con anillo LED
Como mencioné antes, el anillo LED puede consumir hasta 3.6A si todos los LEDs están encendidos al máximo. No queremos sacar tanta corriente del ESP32. En su lugar, necesitamos conectar el anillo LED a una fuente de alimentación externa. El circuito de abajo muestra cómo hacerlo:

El WS2812 funciona a 5V, así que necesitarás una fuente de 5V con suficiente corriente para el anillo LED al brillo máximo que vayas a usar. Con un brillo de 5, medí una corriente de 80mA y con un brillo de 50 la corriente fue de 400mA.
El brillo bajo de 5 es bueno en una habitación oscura y el brillo de 50 es suficiente en una habitación luminosa. Con valores de brillo más altos el reloj puede iluminar una habitación, lo cual no es el objetivo de un reloj. Así que, en mi caso, una fuente de 500mA sería suficiente.
En el circuito de arriba, también añadí un condensador recomendado de 100μ hasta 1000μ en la línea de alimentación. Esto es para estabilizar la fuente en caso de fluctuaciones de corriente al encender o apagar muchos LEDs a la vez.
En la siguiente sección, pasamos del código de prueba a un reloj simulado que nos permite probar diferentes formas y colores para mostrar la hora en el anillo LED.
Código para un reloj simulado con anillo LED
Hay muchas formas diferentes de mostrar la hora en un anillo LED. La imagen de abajo muestra la versión que elegí:

La hora actual (punto naranja) está marcada por un solo LED en la posición correspondiente del reloj. Los minutos (puntos amarillos) se muestran iluminando todos los LEDs hasta el minuto actual. En la imagen de arriba la hora es 11:32, por eso el LED naranja está en la posición de las 11 y los 32 LEDs amarillos muestran los 32 minutos.
Las marcas del reloj se indican con LEDs blancos tenues y el segundo actual se muestra aumentando el brillo del LED en la posición del segundo actual. Es un poco difícil de ver en la imagen de arriba, pero el LED en la posición de las 3 en punto está un poco más brillante. Cómo se hace eso se muestra en el siguiente código.
Ten en cuenta que este código no muestra la hora real, solo simula una hora acelerada para probar la visualización en el anillo LED. Échale un vistazo rápido al código completo antes de comentar los detalles.
#include "Adafruit_NeoPixel.h"
#define DINPIN 3
#define NPIXELS 60
#define OFFSET 29
Adafruit_NeoPixel pixels(NPIXELS, DINPIN, NEO_GRB + NEO_KHZ800);
int index(int i) {
return (i + OFFSET) % NPIXELS;
}
void setPixel(int i, uint32_t color) {
pixels.setPixelColor(index(i), color);
}
uint32_t getPixel(int i) {
return pixels.getPixelColor(index(i));
}
void setTicks(int m) {
uint32_t tickColor = pixels.Color(50, 50, 50);
for (int h = 0; h < 12; h++) {
setPixel(h * 5, tickColor);
}
}
void setHours(int h) {
uint32_t hourColor = pixels.Color(200, 50, 0);
setPixel(h * 5, hourColor); // h: 0..11 = 12 Hours
}
void setMinutes(int m) {
uint32_t minColor = pixels.Color(100, 100, 0);
for (int i = 0; i <= m; i++) {
setPixel(i, minColor);
}
}
void setSeconds(int s) {
uint32_t color = getPixel(s);
setPixel(s, color + 0x373737);
}
void showTime(int h, int m, int s) {
pixels.clear();
setMinutes(m);
setTicks(m);
setHours(h);
setSeconds(s);
pixels.show();
}
void simulateClock() {
for (int h = 0; h < 12; h++) {
for (int m = 0; m < 60; m++) {
for (int s = 0; s < 60; s++) {
showTime(h, m, s);
delay(100);
}
}
}
}
void setup() {
pixels.begin();
pixels.clear();
pixels.setBrightness(10);
pixels.show();
}
void loop() {
simulateClock();
}
En el código de arriba, usamos la librería Adafruit NeoPixel para simular un reloj usando una tira LED de 60 píxeles. El reloj mostrará las horas, minutos y segundos cambiando los colores de píxeles específicos en la tira LED.
Constantes y variables
Como antes, primero definimos las constantes y variables necesarias para la simulación del reloj. Especificamos el pin de datos para la tira LED, el número total de píxeles y un valor de offset para indexar los píxeles.
#include "Adafruit_NeoPixel.h" #define DINPIN 3 #define NPIXELS 60 #define OFFSET 29 Adafruit_NeoPixel pixels(NPIXELS, DINPIN, NEO_GRB + NEO_KHZ800);
Hablemos un poco sobre la constante OFFSET y por qué es necesaria. Si montas el reloj con anillo LED en su marco, el primer LED del anillo (donde se conectan los cables) estará situado un píxel a la izquierda de la posición de las 6 en punto (índice 0).

Así que, si tenemos una hora de 12 en punto (= 0 en punto), no podemos encender el LED/píxel en la posición de índice 0, porque está en la parte inferior. En su lugar, tenemos que encender el LED en el índice 29, ya que esa es la posición de las 12 en punto. Eso significa que siempre necesitamos sumar un offset de 29 a cualquier índice de píxel si queremos encender el LED en la posición correspondiente del reloj.
Para eso sirve la constante OFFSET . Dependiendo de tu marco y la posición mecánica del primer LED del anillo, puede que necesites usar un OFFSET diferente.
Función de índice
La constante OFFSET se usa en la función index() para mapear un índice de tiempo i a una posición de LED en el anillo. Además del offset, también necesitamos calcular el módulo (%) para asegurarnos de que el índice del LED esté en el rango 0..59, ya que solo tenemos 60 LEDs.
int index(int i) {
return (i + OFFSET) % NPIXELS;
}
Por ejemplo, si tenemos una hora de 36 minutos, el LED del anillo que debemos iluminar para mostrar el minuto sería: 36 + 29 % 60 = 5.
Funciones setPixel y getPixel
Las funciones setPixel() y getPixel() usan la función index() para establecer u obtener el color de un índice i en el anillo LED:
void setPixel(int i, uint32_t color) {
pixels.setPixelColor(index(i), color);
}
uint32_t getPixel(int i) {
return pixels.getPixelColor(index(i));
}
Función setTick
La función setTick() marca las horas en el anillo LED. Como un reloj tiene 12 horas pero tenemos 60 LEDs, necesitamos iluminar cada quinto LED del anillo para marcar las horas.
void setTicks(int m) {
uint32_t tickColor = pixels.Color(50, 50, 50);
for (int h = 0; h < 12; h++) {
setPixel(h * 5, tickColor);
}
}
Si miras la función, recorre las 12 horas, multiplica una hora h por 5 y luego usa setPixel() y por tanto index() para convertir la hora en un índice de LED, donde se establece el color.
Yo uso un color blanco tenue (50, 50, 50), pero puedes elegir el color que quieras. Solo asegúrate de que el valor de color no sea mayor de 200, por la forma en que se muestran los segundos. Más sobre eso después.
Función setHours
La función setHours() funciona igual que la función setTicks() , pero muestra una hora específica en vez de todas y usa un color diferente. Yo elegí un color naranja-rojo cálido para la marca de la hora.
void setHours(int h) {
uint32_t hourColor = pixels.Color(200, 50, 0);
setPixel(h * 5, hourColor); // h: 0..11 = 12 Hours
}
Función setMinutes
Mientras que la función setHours() ilumina solo un LED para la hora actual, la función setMinutes() ilumina todos los LEDs hasta el minuto actual. Por eso tenemos el bucle for ahí.
void setMinutes(int m) {
uint32_t minColor = pixels.Color(100, 100, 0);
for (int i = 0; i <= m; i++) {
setPixel(i, minColor);
}
}
Función setSeconds
Por último, queremos mostrar el segundo actual. No quería mostrar el segundo encima de las horas y minutos, así que opté por hacer que el LED del segundo actual sea un poco más brillante, sea cual sea el color que tenga.
void setSeconds(int s) {
uint32_t color = getPixel(s);
setPixel(s, color + 0x373737);
}
La función primero obtiene el color del LED en el segundo actual s llamando a getPixel() . Luego hace el color más brillante sumando 55 a cada valor de color (rojo, verde, azul). 55 en hexadecimal es 0x37 . De ahí viene este valor de 0x373737 . Y esa es la razón por la que el color base no puede ser mayor de 200 , ya que 200 + 55 = 255 , que es el valor máximo de color.
Función showTime
Para mostrar una hora ( h, m, s ) en el reloj simplemente llamamos a las funciones anteriores en este orden y actualizamos el estado de los LEDs con pixels.show() al final.
void showTime(int h, int m, int s) {
pixels.clear();
setMinutes(m);
setTicks(m);
setHours(h);
setSeconds(s);
pixels.show();
}
Función simulateClock
Para probar la visualización de la hora, uso un reloj simulado sencillo que recorre las horas, minutos y segundos 10 veces más rápido ( delay(100) ).
void simulateClock() {
for (int h = 0; h < 12; h++) {
for (int m = 0; m < 60; m++) {
for (int s = 0; s < 60; s++) {
showTime(h, m, s);
delay(100);
}
}
}
}
Funciones setup y loop
Las funciones setup y loop ahora son muy simples. En setup iniciamos el anillo LED. Y en loop simulamos el funcionamiento del reloj.
void setup() {
pixels.begin();
pixels.clear();
pixels.setBrightness(10);
pixels.show();
}
void loop() {
simulateClock();
}
Aparte de mostrar la hora real, ya tenemos todo para mostrar la hora en nuestro anillo LED. Puedes usar este código simulador para encontrar los colores y visualizaciones que más te gusten, sin preocuparte aún de mostrar la hora real.
Para más información sobre los LEDs WS2812 y los efectos que puedes lograr, échale un vistazo a nuestro tutorial How To Control WS2812B Individually Addressable LEDs using Arduino . Ten en cuenta, sin embargo, que en mayo de 2024 no pude hacer funcionar la FastLED library , que se usa en este tutorial, con un ESP32.
En la siguiente sección obtendremos la hora real de un proveedor de hora por internet y usaremos el código anterior para mostrarla en nuestro reloj.
Código para un reloj con anillo LED y hora por internet
Podríamos usar el reloj interno del ESP32 para obtener la hora. Pero eso significa que cada vez que el reloj pierda energía habría que poner la hora manualmente. Además, habría que ajustar la hora dos veces al año cuando cambie el horario de verano.
Eso implicaría tener botones o software extra (interfaz web, app de móvil) para poder ajustar la hora. Todo eso es un poco engorroso. Prefiero tener un reloj totalmente automático obteniendo siempre la hora exacta de un proveedor de hora por internet.
Si quieres saber cómo funciona eso en detalle, échale un vistazo a nuestro tutorial Automatic Daylight Savings Time Clock . Básicamente copié y pegué el código de ahí en el código de abajo.
#include "WiFi.h"
#include "HTTPClient.h"
#include "ArduinoJson.h"
#include "TimeLib.h"
#include "Adafruit_NeoPixel.h"
#define WIFI_SSID "YOUR_SSID"
#define WIFI_PASSPHRASE "YOUR_PASSWORD"
#define URL "http://worldtimeapi.org/api/ip"
#define DINPIN 3
#define NPIXELS 60
#define OFFSET 29
Adafruit_NeoPixel pixels(NPIXELS, DINPIN, NEO_GRB + NEO_KHZ800);
StaticJsonDocument<2048> doc;
int index(int i) {
return (i + OFFSET) % NPIXELS;
}
void setPixel(int i, uint32_t color) {
pixels.setPixelColor(index(i), color);
}
uint32_t getPixel(int i) {
return pixels.getPixelColor(index(i));
}
void setTicks(int m) {
uint32_t tickColor = pixels.Color(50, 50, 50);
for (int h = 0; h < 12; h++) {
setPixel(h * 5, tickColor);
}
}
void setHours(int h) {
uint32_t hourColor = pixels.Color(200, 50, 0);
setPixel(h * 5, hourColor); // h: 0..11 = 12 Hours
}
void setMinutes(int m) {
uint32_t minColor = pixels.Color(100, 100, 0);
for (int i = 0; i <= m; i++) {
setPixel(i, minColor);
}
}
void setSeconds(int s) {
uint32_t color = getPixel(s);
setPixel(s, color + 0x373737);
}
void showTime(int h, int m, int s) {
pixels.clear();
setMinutes(m);
setTicks(m);
setHours(h);
setSeconds(s);
pixels.show();
}
void updateDisplay() {
time_t t = now();
showTime(hourFormat12(t), minute(t), second(t));
}
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 setup() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSPHRASE);
while (WiFi.status() != WL_CONNECTED)
delay(500);
pixels.begin();
pixels.clear();
pixels.setBrightness(10);
pixels.show();
}
void loop() {
if (shouldSyncTime())
syncTime();
updateDisplay();
delay(100);
}
Antes de poder usar este código tendrás que instalar dos librerías adicionales, que son » ArduinoJson.h » y » TimeLib.h «. » WiFi.h » y » HTTPClient.h » forman parte de la librería estándar ESP32/Arduino y no necesitas instalarlas aparte.
También tendrás que poner el SSID y la contraseña de tu red WiFi. Así el ESP32 podrá conectarse al proveedor de hora por internet en la URL indicada:
#define WIFI_SSID "YOUR_SSID" #define WIFI_PASSPHRASE "YOUR_PASSWORD" #define URL "http://worldtimeapi.org/api/ip"
Con eso listo, vamos a ver más de cerca las nuevas funciones añadidas al código.
Función syncTime
La función syncTime() se conecta al proveedor de hora por internet, lee los datos en formato JSON, extrae la información relevante de la hora (h, m, s, D, M, Y) y ajusta el reloj interno del ESP32 en consecuencia.
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();
}
Función shouldSyncTime
No queremos consultar al proveedor de hora por internet demasiado a menudo y que nos bloqueen. Por eso limito la sincronización del reloj interno del ESP32 con el proveedor de hora a una vez por hora. Específicamente, sincronizamos en el tercer segundo de cada hora. La función shouldSyncTime() nos indica cuándo toca hacerlo.
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;
}
Función updateDisplay
La función updateDisplay() sustituye a la función simulateClock() que usábamos antes. Lee el reloj interno del ESP32, que ya hemos sincronizado con la hora de internet, y llama a showTime() para mostrarla en el anillo LED.
void updateDisplay() {
time_t t = now();
showTime(hourFormat12(t), minute(t), second(t));
}
Función setup
En la función setup() añadimos la funcionalidad de conectar a la red WiFi, pero por lo demás inicializamos el anillo LED igual que antes.
void setup() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSPHRASE);
while (WiFi.status() != WL_CONNECTED)
delay(500);
pixels.begin();
pixels.clear();
pixels.setBrightness(10);
pixels.show();
}
Función loop
En la función loop() primero comprobamos si debemos sincronizar la hora. Si es así, llamamos a syncTime() para hacerlo. Pero en cualquier caso llamamos a updateDisplay() para mostrar la hora actual en el anillo LED.
void loop() {
if (shouldSyncTime())
syncTime();
updateDisplay();
delay(100);
}
Como tenemos un delay(100) en el loop, esta actualización ocurre cada 100ms, es decir, 10 veces por segundo.
¡Y eso es todo! Con esto tienes un reloj totalmente automático y siempre preciso que muestra la hora en un anillo LED. En las siguientes secciones añadimos atenuación automática y activación por movimiento para mejorarlo aún más.
Atenuación automática del reloj con anillo LED
Los LEDs WS2812 son muy brillantes. Normalmente no los quieres tan brillantes por la noche. Por otro lado, si ajustas el brillo para que sea adecuado en una habitación oscura, el reloj será difícil de leer durante el día. Sería ideal ajustar el brillo de los LEDs automáticamente según la luz ambiental.
Para eso, añadimos una resistencia fotosensible (LDR) al circuito, leemos su valor por una entrada analógica del ESP32 y llamamos a setBrightness() para ajustar el brillo general del anillo LED.
Circuito del reloj con anillo LED y LDR
La imagen de abajo muestra cómo conectar el sensor de luz (LDR) al circuito existente. Un pin del LDR se conecta a 5V y el otro a un potenciómetro de 10KΩ, que a su vez va a tierra.

La salida del LDR (cable verde) se conecta al GPIO0 del ESP32. Cualquier otro pin GPIO también sirve siempre que pueda leer una señal analógica.
El LDR y el potenciómetro de 10KΩ forman un divisor de voltaje. Para más detalles sobre cómo funciona esto, échale un vistazo a nuestro tutorial How to detect light using an Arduino , de donde proviene este circuito.
Dependiendo del brillo de la luz ambiente, el circuito del LDR producirá un voltaje proporcional en el rango de 0V a 5V. En la práctica, nunca se obtiene el rango completo, ya que nunca habrá oscuridad total ni máxima luz. El potenciómetro de 10KΩ te permite ajustar el punto de trabajo del voltaje de salida. Mapearemos los cambios de voltaje a un rango adecuado en el código de abajo.
Código de prueba para ajustar el rango de brillo
Antes de añadir la función de atenuación automática a nuestro reloj con anillo LED, primero necesitaremos ajustar los parámetros. Dependiendo de la luz ambiente, queremos un valor de brillo entre 5, cuando está muy oscuro, y quizá 50, cuando hay mucha luz.
Sin embargo, lo que leemos del sensor de luz en la entrada analógica serán valores entre 0 y 4095. Eso dependerá de la resistencia por defecto del LDR, el ajuste del potenciómetro y las condiciones de luz ambiente.
El siguiente código de prueba te permite encontrar la correspondencia adecuada entre los valores del sensor LDR y los valores de brillo que queremos establecer.
#define LDRPIN 0
void setup() {
Serial.begin(112500);
pinMode(LDRPIN, INPUT);
}
void loop() {
int ldrValue = analogRead(LDRPIN);
Serial.print(ldrValue);
int brightness = map(ldrValue, 1400, 3000, 5, 50);
Serial.print(" -> ");
Serial.println(brightness);
delay(1000);
}
Lee el valor del LDR ( ldrValue ), lo imprime, lo mapea a un valor de brillo ( brightness ) y también lo imprime. Si subes y ejecutas este código, deberías ver algo así en el Monitor Serie.

Si tapas el sensor de luz (oscuridad), deberías ver un valor de brillo cercano a 5, y si lo expones a luz muy brillante (por ejemplo, luz solar), deberías obtener un valor de brillo cercano a 50. Para lograrlo, tendrás que ajustar los parámetros fromLow , fromHigh de la función map:
map(value, fromLow, fromHigh, toLow, toHigh)
Para mi sensor LDR, el ajuste del potenciómetro y las condiciones de luz, terminé con fromLow=1400 y fromHigh=3000 , como puedes ver en el código de arriba. Tus valores serán diferentes, pero puedes usar los míos como punto de partida.
De forma similar, si quieres un rango de brillo diferente a 5..50 , puedes elegir otros valores para toLow y toHigh . Incluso podrías apagar el reloj por la noche eligiendo toLow=0 .
Añadiendo el código de atenuación automática
Una vez que hayas encontrado unos parámetros adecuados, puedes añadir la siguiente función de atenuación automática al código del reloj.
void updateBrightness() {
if (millis() % 1000 < 100) {
int ldrValue = analogRead(LDRPIN);
int brightness = map(ldrValue, 1400, 3000, 5, 50);
pixels.setBrightness(constrain(brightness, 5, 50));
}
}
Ajusta el brillo de todo el anillo LED según el valor que leemos en el LDRPIN . Sin embargo, no queremos ajustar el brillo cada vez que actualizamos el reloj, lo cual ocurre cada 100ms. Esto podría causar un parpadeo molesto, ya que cualquier pequeño cambio en la luz ambiente podría cambiar el brillo de los LEDs.
Por eso actualizamos el brillo solo una vez por segundo, lo que se consigue con millis() % 1000 < 100. Si quieres menos o más actualizaciones que cada 1000ms, solo cambia el 1000 por el valor que prefieras. Ten en cuenta que el umbral de < 100 está relacionado con el retardo de 100ms en el bucle principal.
También fíjate en la llamada extra a c onstrain(brightness, 5, 50), que asegura que el brillo esté en el rango 5..50 , lo cual no está garantizado por la función map() .
Para integrar la atenuación en el código del reloj, básicamente solo hay que añadir una llamada a la función updateBrightness() en el bucle principal:
void loop() {
if (shouldSyncTime())
syncTime();
updateBrightness();
updateDisplay();
delay(100);
}
Por supuesto, también está la definición de la constante LDRPIN y la configuración de LDRPIN como entrada. El código completo para el reloj con anillo LED y atenuación automática se muestra abajo:
#include "WiFi.h"
#include "HTTPClient.h"
#include "ArduinoJson.h"
#include "TimeLib.h"
#include "Adafruit_NeoPixel.h"
#define WIFI_SSID "YOUR_SSID"
#define WIFI_PASSPHRASE "YOUR_PASSWORD"
#define URL "http://worldtimeapi.org/api/ip"
#define LDRPIN 0
#define DINPIN 3
#define NPIXELS 60
#define OFFSET 29
Adafruit_NeoPixel pixels(NPIXELS, DINPIN, NEO_GRB + NEO_KHZ800);
StaticJsonDocument<2048> doc;
int index(int i) {
return (i + OFFSET) % NPIXELS;
}
void setPixel(int i, uint32_t color) {
pixels.setPixelColor(index(i), color);
}
uint32_t getPixel(int i) {
return pixels.getPixelColor(index(i));
}
void setTicks(int m) {
uint32_t tickColor = pixels.Color(50, 50, 50);
for (int h = 0; h < 12; h++) {
setPixel(h * 5, tickColor);
}
}
void setHours(int h) {
uint32_t hourColor = pixels.Color(200, 50, 0);
setPixel(h * 5, hourColor); // h: 0..11 = 12 Hours
}
void setMinutes(int m) {
uint32_t minColor = pixels.Color(100, 100, 0);
for (int i = 0; i <= m; i++) {
setPixel(i, minColor);
}
}
void setSeconds(int s) {
uint32_t color = getPixel(s);
setPixel(s, color + 0x373737);
}
void showTime(int h, int m, int s) {
pixels.clear();
setMinutes(m);
setTicks(m);
setHours(h);
setSeconds(s);
pixels.show();
}
void updateDisplay() {
time_t t = now();
showTime(hourFormat12(t), minute(t), second(t));
}
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 updateBrightness() {
if (millis() % 1000 < 100) {
int ldrValue = analogRead(LDRPIN);
int brightness = map(ldrValue, 1400, 3000, 5, 50);
pixels.setBrightness(constrain(brightness, 5, 50));
}
}
void setup() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSPHRASE);
while (WiFi.status() != WL_CONNECTED)
delay(500);
pinMode(LDRPIN, INPUT);
pixels.begin();
pixels.clear();
pixels.setBrightness(10);
pixels.show();
}
void loop() {
if (shouldSyncTime())
syncTime();
updateBrightness();
updateDisplay();
delay(100);
}
¡Uf, ya casi terminamos! Como último detalle, vamos a hacer que el reloj se encienda automáticamente si detecta movimiento.
Activación automática del reloj con anillo LED
El reloj con anillo LED consume bastante energía y existe el riesgo de que los LEDs se quemen si el reloj está encendido todo el tiempo. Pero no tiene sentido mostrar la hora si no hay nadie para verla. Por eso vamos a añadir un sensor de movimiento (PIR) al circuito. Encenderá el reloj durante un periodo determinado (por ejemplo, 10 segundos) si detecta movimiento y después apagará el anillo LED.
La siguiente imagen muestra cómo añadir el sensor de movimiento PIR al circuito existente. Solo tienes que conectar 5V y GND a los pines correspondientes del sensor PIR (cables rojo y azul). La salida del sensor (cable morado) se conecta al GPIO10 del ESP32.

Aquí tienes una foto del circuito completo en una breadboard. Como puedes ver, pude hacer funcionar el reloj con anillo LED para pruebas usando una pila de 9V (con un regulador de 5V).

Sin embargo, si realmente quieres usar el reloj con batería, te recomiendo usar una batería USB externa. También puedes poner el ESP32 en deep-sleep mientras el reloj está inactivo. Para más detalles sobre eso, échale un vistazo a nuestro tutorial sobre How to Build a Motion Activated Night Light .
Antes de integrar la activación automática en el código del reloj, vamos a escribir primero un código de prueba para el sensor PIR.
Código de prueba para el sensor PIR
El siguiente código lee la señal del sensor PIR e imprime «motion detected» si se detecta movimiento o «nothing» en caso contrario.
#define PIRPIN 10
unsigned long lastMotion= 0;
bool motionDetected() {
if (digitalRead(PIRPIN))
lastMotion = millis();
unsigned long diff = millis() - lastMotion;
return diff < 10000 && diff >= 0;
}
void setup() {
Serial.begin(115200);
pinMode(PIRPIN, INPUT);
}
void loop() {
if (motionDetected()) {
Serial.println("motion detected");
} else {
Serial.println("nothing");
}
delay(100);
}
El sensor de movimiento que uso aquí es el AM312 , que solo da una señal alta durante unos 2 segundos después de detectar el primer movimiento. Para más detalles sobre el AM312 y cómo usarlo, échale un vistazo a nuestro tutorial How to Build a Motion Activated Night Light .
Si usamos la señal del AM312 directamente en nuestro código, el reloj solo se activaría durante 2 segundos y luego se apagaría si no hay más movimiento. Eso haría que el reloj se encendiera y apagara constantemente, lo cual no queda bien. El código de arriba por eso añade un temporizador que hace que motionDetected() devuelva true durante al menos 10000ms = 10 segundos. Puedes alargar o acortar ese periodo como prefieras.
Si montas el circuito, subes y ejecutas el código y funciona como esperas, ya puedes integrar el código de activación automática en el código del reloj.
Añadiendo el código de activación automática
Para la integración, vamos a añadir dos funciones y también cambiaremos un poco el bucle principal.
Función motionDetected
La función motionDetected() se basa en el código de prueba anterior y devuelve true durante al menos 10 segundos después de detectar el primer movimiento. El temporizador de 10 segundos se reinicia cada vez que se detecta nuevo movimiento durante ese periodo. Así el reloj permanece encendido mientras haya alguien cerca y moviéndose.
bool motionDetected() {
if (digitalRead(PIRPIN))
lastMotion = millis();
unsigned long diff = millis() - lastMotion;
return diff < 10000 && diff >= 0;
}
Fíjate en la condición diff >= 0 en la sentencia return. Es necesaria porque el valor del temporizador que devuelve millis() wrap después de varios días (49 en un Arduino UNO) y la diferencia de tiempo diff sería negativa.
Función clearDisplay
Si no se detecta movimiento, apagamos los LEDs del reloj llamando a clearDisplay() . Esta función simplemente borra todos los valores de los píxeles y luego actualiza el estado de los LEDs llamando a show() .
void clearDisplay() {
pixels.clear();
pixels.show();
}
Función loop
Por último, necesitamos cambiar la función loop para reaccionar al sensor de movimiento. Si detectamos movimiento con motionDetected() , ejecutamos la actualización típica del reloj. Si no, apagamos la pantalla del reloj con clearDisplay() . Todo esto ocurre cada 1/10 de segundo gracias al delay(100) .
void loop() {
if (motionDetected()) {
if (shouldSyncTime())
syncTime();
updateBrightness();
updateDisplay();
} else {
clearDisplay();
}
delay(100);
}
Y eso es todo. Con esto ya tenemos todas las piezas necesarias. Abajo tienes el código completo para el reloj activado por movimiento.
Código completo para el reloj activado por movimiento
#include "WiFi.h"
#include "HTTPClient.h"
#include "ArduinoJson.h"
#include "TimeLib.h"
#include "Adafruit_NeoPixel.h"
#define WIFI_SSID "YOUR_SSID"
#define WIFI_PASSPHRASE "YOUR_PASSWORD"
#define URL "http://worldtimeapi.org/api/ip"
#define PIRPIN 10
#define LDRPIN 0
#define DINPIN 3
#define NPIXELS 60
#define OFFSET 29
Adafruit_NeoPixel pixels(NPIXELS, DINPIN, NEO_GRB + NEO_KHZ800);
StaticJsonDocument<2048> doc;
unsigned long lastMotion = 0;
int index(int i) {
return (i + OFFSET) % NPIXELS;
}
void setPixel(int i, uint32_t color) {
pixels.setPixelColor(index(i), color);
}
uint32_t getPixel(int i) {
return pixels.getPixelColor(index(i));
}
void setTicks(int m) {
uint32_t tickColor = pixels.Color(50, 50, 50);
for (int h = 0; h < 12; h++) {
setPixel(h * 5, tickColor);
}
}
void setHours(int h) {
uint32_t hourColor = pixels.Color(200, 50, 0);
setPixel(h * 5, hourColor); // h: 0..11 = 12 Hours
}
void setMinutes(int m) {
uint32_t minColor = pixels.Color(100, 100, 0);
for (int i = 0; i <= m; i++) {
setPixel(i, minColor);
}
}
void setSeconds(int s) {
uint32_t color = getPixel(s);
setPixel(s, color + 0x373737);
}
void showTime(int h, int m, int s) {
pixels.clear();
setMinutes(m);
setTicks(m);
setHours(h);
setSeconds(s);
pixels.show();
}
void updateDisplay() {
time_t t = now();
showTime(hourFormat12(t), minute(t), second(t));
}
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 updateBrightness() {
if (millis() % 1000 < 100) {
int ldrValue = analogRead(LDRPIN);
int brightness = map(ldrValue, 1400, 3000, 5, 50);
pixels.setBrightness(constrain(brightness, 5, 50));
}
}
bool motionDetected() {
if (digitalRead(PIRPIN))
lastMotion = millis();
unsigned long diff = millis() - lastMotion;
return diff < 10000 && diff >= 0;
}
void clearDisplay() {
pixels.clear();
pixels.show();
}
void setup() {
WiFi.mode(WIFI_STA);
WiFi.begin(WIFI_SSID, WIFI_PASSPHRASE);
while (WiFi.status() != WL_CONNECTED)
delay(500);
pinMode(PIRPIN, INPUT);
pinMode(LDRPIN, INPUT);
pixels.begin();
pixels.clear();
pixels.setBrightness(10);
pixels.show();
}
void loop() {
if (motionDetected()) {
if (shouldSyncTime())
syncTime();
updateBrightness();
updateDisplay();
} else {
clearDisplay();
}
delay(100);
}
Conclusiones
En este tutorial has aprendido a construir un reloj con anillo LED activado por movimiento, con atenuación automática y sincronización de hora por internet. Tiene la ventaja de que siempre es preciso y totalmente automático. No necesitas botones ni mandos para encenderlo o apagarlo, poner la hora o ajustar el brillo.
Además de construir un mejor marco para el reloj y cambiar los colores de los LEDs a tu gusto, hay algunas mejoras más que podrías añadir. Por ejemplo, podrías añadir un sensor de temperatura que ajuste el color de los LEDs para indicar la temperatura ambiente. Por ejemplo, más azulados cuando hace frío y más rojizos cuando hace calor.
Como el reloj se activa por movimiento, sería posible hacerlo funcionar con batería, especialmente si pones el ESP32 en deep-sleep mientras la pantalla está inactiva. Échale un vistazo a nuestro tutorial sobre How to Build a Motion Activated Night Light como ejemplo.
Por último, también podrías alternar cada pocos segundos entre mostrar la hora y mostrar algún tipo de representación de la fecha actual, ya que también estamos descargando la información de la fecha del proveedor de hora por internet. En vez de worldtimeapi.org, también podrías usar un servidor SNTP que te permita sincronizar la hora más a menudo. Mira nuestro tutorial sobre How to synchronize ESP32 clock with SNTP server .
Y si no te convence nada el reloj con anillo LED, échale un vistazo a este tutorial Digital Clock with CrowPanel 3.5″ ESP32 Display , que explica cómo mostrar la hora y la fecha en una pantalla, como un reloj digital convencional.
¡Hay un montón de cosas para experimentar!
Si tienes cualquier pregunta, no dudes en dejarla. ¡Encantado de ayudar ; )

