Skip to Content

Reloj de anillo LED con WS2812

Reloj de anillo LED con WS2812

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).

LED Ring Clock in action
Reloj con anillo de LEDs en acción

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

Dupont wire set

Set de cables Dupont

Half_breadboard56a

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).

5050 RBG LED
LED RGB 5050 ( source )

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.

WS2812 RGB LED Strip
Tira LED RGB WS2812 ( source )

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:

WS2812 RGB LED Rings
Anillos LED RGB WS2812 ( source )

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:

WS2812 RGB LED Ring in four parts
Anillo LED RGB WS2812 en cuatro partes

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:

Input and Output of an WS2812 LED Strip
Entrada y salida de una tira LED WS2812

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í:

Cableado del anillo LED WS2812

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í.

Wiring of LED Ring Segments
Cableado de los segmentos del anillo LED

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

Wiring of complete LED Ring
Cableado del anillo LED completo

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.

Parts of the LED Clock Frame
Partes del marco del reloj LED

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

LED Clock Frame
Marco del reloj LED

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.

Connecting ESP32 to WS2812 LED Ring
Conexión del ESP32 al anillo LED 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:

Connecting WS2812 LED Ring to external power supply
Conexión del anillo LED WS2812 a fuente de alimentación externa

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í:

Showing time on an LED Ring
Mostrando la hora en un anillo LED

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).

Offset for pixel index
Offset para el índice de píxel

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.

Wiring of LDR with LED Ring Clock
Cableado del LDR con el reloj de anillo LED

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.

Serial output of test code to adjust brightness
Salida serie del código de prueba para ajustar el brillo

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.

Wiring of PIR sensor with LED Ring Clock
Cableado del sensor PIR con el reloj de anillo LED

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).

LED Ring Clock with PIR sensor and Battery
Reloj con anillo LED, sensor PIR y batería

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