Neste tutorial vais aprender a construir um relógio de LED Ring com a fita de LEDs WS2812 e um ESP32. Vamos adicionar regulação automática do brilho ao relógio e também usar um fornecedor de hora via internet para garantir que está sempre certo. Por fim, o relógio será ativado por movimento, usando um sensor PIR, para não desperdiçar energia desnecessariamente.
O pequeno vídeo abaixo mostra o relógio em funcionamento. Podes ver as marcas das horas (pontos brancos) e os segundos a passar (ponto branco em movimento).

A hora atual é marcada pelo ponto laranja e os minutos pelos pontos amarelos. Assim, na imagem acima, o relógio mostra as 11:36.
Ao sincronizar o relógio por Wi-Fi com um fornecedor de hora na internet, garantimos que o nosso relógio mostra sempre a hora correta, independentemente de mudanças de horário de verão, falhas de energia ou de um relógio interno impreciso.
Vamos começar pelos componentes necessários.
Componentes Necessários
Aqui estão os componentes necessários para o projeto. Em vez da placa de desenvolvimento ESP32-C3 Mini listada abaixo, usei uma placa muito semelhante chamada ESP32-C3 SuperMini da AliExpress. A minha tinha apenas um LED incorporado de uma cor, enquanto a placa abaixo tem um LED RGB. Mas tirando isso, devem ser praticamente idênticas e ambas devem funcionar. Qualquer outro ESP32 ou ESP8266 também serve. No entanto, se quiseres usar um Arduino, vais precisar de um Wi-Fi shield.

ESP32-C3 Mini

Cabo USB C

LED Ring RGB

Conjunto de fios Dupont

Breadboard

Kit de resistores & LED

Conjunto de resistores variáveis

Sensor de Movimento

Conjunto de fotoresistores
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 da Fita de LED RGB WS2812
Vamos usar um anel de LED RGB WS2812 para o nosso relógio. O WS2812 é um tipo específico de LED RGB baseado no tamanho de encapsulamento 5050. O LED RGB 5050 refere-se às dimensões do encapsulamento do LED, que são 5,0mm x 5,0mm. A imagem abaixo mostra um LED 5050 com o seu IC de controlo e os três LEDs (verde, vermelho, azul).

Nota que existem outros controladores de fitas de LED RGB semelhantes, como o WS2811, SK6812 e WS2815. Este quick guide contém uma boa comparação entre os diferentes tipos. Aqui vamos usar o WS2812.
O WS2812 funciona a 5V, tem um controlador PWM incorporado para mistura de cores e pode ser ligado em série, usando apenas uma linha de comunicação para criar fitas de LED mais longas. O mais comum é encontrares o WS2812 em fitas flexíveis de LED RGB como a que está mostrada abaixo.

Estas fitas de LED existem com vários números de LEDs e densidades e podem ser cortadas à medida. Além das fitas flexíveis, também podes encontrá-las em forma de anéis rígidos:

Os anéis mais pequenos vêm numa só peça, mas o anel grande com 60 LEDs que vamos usar para o nosso relógio vem em quatro partes que terás de soldar. A imagem abaixo mostra a parte de trás das quatro peças:

Como montar os quatro segmentos num anel grande é o tema da próxima secção.
Construção do Relógio de LED Ring
Como uma hora tem 60 minutos e um minuto 60 segundos, queremos usar um anel com 60 LEDs. Como mencionado acima, devido ao seu tamanho, vem em 4 secções (cada uma com 15 LEDs) que precisamos de soldar.
Nota que as fitas e anéis de LED RGB WS2812 têm um lado de entrada (DIN) e um de saída (DO), como mostrado na imagem abaixo:

Começa por soldar o fio de sinal (amarelo) ao pad DIN de um dos segmentos do anel de LED. Depois solda um fio de massa (preto) ao pad GND e um fio de alimentação (vermelho) ao pad 5V. Deve ficar assim:

Por fim, precisamos de ligar os quatro segmentos. Liga GND a GND, 5V a 5V e DOUT a DIN em cada um dos segmentos. Uma ligação entre segmentos deve ficar assim.

Basicamente, não consegues ligar os pads de forma errada, caso contrário não conseguirias um anel perfeito:

Nota que fitas de LED longas sofrem de queda de tensão, o que faz com que os LEDs no final da fita fiquem menos brilhantes do que os do início. Por vezes, por isso, encontras fitas de LED com alimentação em ambas as extremidades.
Com o anel de 60 LEDs, não notei diferença de brilho entre o primeiro e o último LED. No entanto, se notares, podes ligar os pads GND e 5V entre o último e o primeiro segmento de LED. No meu caso, não foi necessário.
Também podes achar a soldadura um pouco difícil, pois os pads são muito pequenos. Ajuda colocar os segmentos do anel de LED num suporte para os fixar. Usei uma impressora 3D para criar o suporte ou estrutura do relógio mostrada na próxima secção.
Estrutura do Relógio de LED
A Estrutura do Relógio de LED é composta por três partes. A traseira, onde está o anel de LED, uma frente transparente e uma base que permite manter o anel de LED de pé.

A Estrutura do Relógio de LED completa fica assim e podes encontrar o STL files here :

O próximo passo é ligar o anel de LED ao ESP32.
Ligações do Relógio de LED Ring
Ligar o anel de LED WS2812 ao ESP32 é simples. Primeiro liga o GND do ESP32 ao GND do anel de LED (fio azul). Depois liga o 5V do ESP32 à entrada 5V do anel de LED WS2812. E finalmente liga o GPIO3 do ESP32, através de um resistor de 220Ω, ao DIN do WS2812.

Podes experimentar sem o resistor de 220Ω para testar, mas é mais seguro tê-lo. Ele protege o GPIO do ESP32 de sobrecorrentes. Em vez do GPIO3 podes usar qualquer outro pino GPIO, desde que suporte PWM.
Atenção que o anel de LED pode consumir muita corrente! Um único LED RGB é composto por três LEDs (vermelho, verde, azul) e cada um consome até 20mA quando está totalmente ligado. Ou seja, se um LED RGB estiver totalmente ativado (luz branca), consome até 60mA (medi 45mA). Multiplica 60mA por 60 LEDs e obtemos uma corrente de 3,6A , se todos os LEDs do anel estiverem ligados ao máximo!
Isso é normalmente demasiada corrente para tirar do ESP32 ou da porta USB. O código de teste na próxima secção define o brilho do anel de LED para um valor baixo de 10, que é mais do que suficiente para testar.
Se precisares de mais informações sobre a placa SuperMini que estou a usar aqui, vê o nosso tutorial ESP32-C3 SuperMini Board .
Código de Teste para o Anel de LED
Antes de tentares implementar algo mais complexo, é melhor testar o funcionamento do anel de LED com um código simples primeiro. O código abaixo liga sequencialmente todos os 60 LEDs e, quando todos estão ligados, desliga-os novamente. Isto permite verificar se todos os LEDs funcionam e se os segmentos do anel estão corretamente ligados.
#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);
}
}
No excerto de código acima, estamos a usar a Adafruit NeoPixel library . Se ainda não o fizeste, vais precisar de install the library primeiro, antes de poderes usar este código.
Vamos analisar o código de teste para perceber melhor o seu funcionamento.
Constantes e Variáveis
Primeiro definimos a constante DINPIN que especifica o pino de entrada de dados ao qual o anel de LED está ligado, e NUMPIXELS que define o número total de Pixels/LEDs na fita. No nosso caso temos 60 LEDs e usamos o GPIO 3.
#define DINPIN 3 #define NUMPIXELS 60
Função setup
Na função setup() , inicializamos a fita do anel de LED chamando pixels.begin() , limpamos quaisquer cores de pixel existentes com pixels.clear() , definimos o nível de brilho para 10 com pixels.setBrightness(10) , e finalmente mostramos o estado dos pixels usando pixels.show() .
void setup() {
pixels.begin();
pixels.clear();
pixels.setBrightness(10);
pixels.show();
}
Como referi antes, tem cuidado ao mudar o brilho! No brilho máximo, o anel de LED pode consumir demasiada corrente para o teu ESP32, porta USB ou qualquer fonte de alimentação que estejas a usar.
Função loop
A função loop() contém um ciclo que primeiro liga todos os LEDs a branco, percorrendo cada pixel com um ciclo for. A função pixels.setPixelColor() é usada para definir a cor de cada pixel para branco (255, 255, 255) e depois pixels.show() é chamada para mostrar o estado dos LEDs. É adicionado um atraso de 200ms entre cada atualização de pixel.
Depois de todos os pixels estarem a branco, outro ciclo é usado para desligar todos os LEDs, definindo a cor para preto (0, 0, 0) e atualizando a fita com pixels.show() novamente com um atraso de 200ms entre cada atualização.
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);
}
}
Se isso funcionar, parabéns! Na próxima secção, vamos tentar algo um pouco mais complexo, simulando a hora para o anel de LED mostrar e também melhorar um pouco o nosso circuito.
Melhoria das Ligações do Relógio de LED Ring
Como mencionado acima, o anel de LED pode consumir até 3,6A quando todos os LEDs estão ligados ao máximo. Não queremos tirar tanta corrente do ESP32. Em vez disso, precisamos de ligar o anel de LED a uma fonte de alimentação externa. O circuito abaixo mostra como fazer isso:

O WS2812 funciona a 5V, por isso vais precisar de uma fonte de 5V com corrente suficiente para o anel de LED no teu brilho máximo. Com brilho a 5, medi uma corrente de 80mA e com brilho a 50 a corrente foi de 400mA.
O brilho baixo de 5 é bom para um quarto escuro e o brilho de 50 é mais do que suficiente para um quarto iluminado. Com valores de brilho mais altos, o relógio pode iluminar um quarto, o que não é o objetivo de um relógio. Por isso, no meu caso, uma fonte de 500mA seria suficiente.
No circuito acima, também adicionei um condensador recomendado de 100μ até 1000μ na linha de alimentação. Isto serve para estabilizar a fonte de alimentação em caso de flutuações de corrente causadas por ligar ou desligar muitos LEDs ao mesmo tempo.
Na próxima secção, passamos do código de teste para um relógio simulado que nos permite experimentar diferentes formas e cores de mostrar a hora num anel de LED.
Código para um Relógio de LED Ring Simulado
Existem várias formas de mostrar a hora num anel de LED. A imagem abaixo mostra a versão que escolhi:

A hora atual (ponto laranja) é marcada por um único LED na posição correspondente do relógio. Os minutos (pontos amarelos) são mostrados iluminando todos os LEDs até ao minuto atual. Na imagem acima, a hora é 11:32, por isso o LED laranja na posição das 11 horas está ligado e os 32 LEDs amarelos mostram os 32 minutos.
Os traços do mostrador do relógio são indicados por LEDs brancos fracos e o segundo atual é mostrado tornando o LED na posição do segundo mais brilhante. É um pouco difícil de ver na imagem acima, mas o LED na posição das 3 horas (segundos) está ligeiramente mais brilhante. Como isso é feito, é mostrado no código seguinte.
Nota que este código não mostra a hora real, apenas simula uma hora acelerada para testar a visualização da hora no anel de LED. Dá uma vista de olhos ao código completo antes de discutirmos os detalhes.
#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();
}
No código acima, estamos a usar a biblioteca Adafruit NeoPixel para simular um relógio usando uma fita de LED com 60 pixels. O relógio vai mostrar as horas, minutos e segundos mudando as cores de pixels específicos na fita de LED.
Constantes e Variáveis
Como antes, primeiro definimos as constantes e variáveis necessárias para a simulação do relógio. Especificamos o pino de dados para a fita de LED, o número total de pixels e um valor de offset para indexar os pixels.
#include "Adafruit_NeoPixel.h" #define DINPIN 3 #define NPIXELS 60 #define OFFSET 29 Adafruit_NeoPixel pixels(NPIXELS, DINPIN, NEO_GRB + NEO_KHZ800);
Vamos falar um pouco sobre a constante OFFSET e porque é necessária. Se montares o Relógio de LED Ring na sua estrutura, o primeiro LED do anel (onde os fios estão ligados) ficará um pixel à esquerda da posição das 6 horas (índice 0).

Assim, se tivermos uma hora de 12 horas (= 0 horas), não podemos ligar o LED/pixel na posição de índice 0, porque esse está em baixo. Em vez disso, temos de ligar o LED no índice 29, pois esse é a posição das 12 horas. Ou seja, temos sempre de adicionar um offset de 29 a qualquer índice de pixel, se quisermos ligar o LED na posição correspondente do relógio.
É para isso que serve a constante OFFSET . Dependendo da tua estrutura do relógio e da posição mecânica do primeiro LED do anel, podes precisar de usar um OFFSET diferente.
Função de índice
A constante OFFSET é usada na função index() para mapear um índice de tempo i para uma posição de LED no anel. Além do offset, também precisamos de calcular o módulo (%) para garantir que o índice do LED está no intervalo 0..59, já que só temos 60 LEDs.
int index(int i) {
return (i + OFFSET) % NPIXELS;
}
Por exemplo, se tivermos 36 minutos, o LED no anel que precisamos de iluminar para mostrar o minuto seria no índice: 36 + 29 % 60 = 5.
Funções setPixel e getPixel
As funções setPixel() e getPixel() usam a função index() para definir ou obter a cor para um determinado índice i no anel de LED:
void setPixel(int i, uint32_t color) {
pixels.setPixelColor(index(i), color);
}
uint32_t getPixel(int i) {
return pixels.getPixelColor(index(i));
}
Função setTick
A função setTick() marca os traços horários no anel de LED. Como um dia num relógio tem 12 horas mas temos 60 LEDs, precisamos de iluminar cada 5º LED no anel para marcar as horas.
void setTicks(int m) {
uint32_t tickColor = pixels.Color(50, 50, 50);
for (int h = 0; h < 12; h++) {
setPixel(h * 5, tickColor);
}
}
Se olhares para a função, ela percorre as 12 horas, multiplica uma hora h por 5 e depois usa setPixel() e portanto index() para converter a hora num índice de LED, onde a cor é definida.
Estou a usar uma cor branca fraca (50, 50, 50), mas podes escolher a cor que quiseres. Só tens de garantir que o valor da cor não é maior que 200, devido à forma como os segundos são mostrados. Mais sobre isso a seguir.
Função setHours
A função setHours() funciona tal como a função setTicks() , mas mostra uma hora específica em vez de todas as horas e usa uma cor diferente. Escolhi uma cor laranja-avermelhada para a marca da hora.
void setHours(int h) {
uint32_t hourColor = pixels.Color(200, 50, 0);
setPixel(h * 5, hourColor); // h: 0..11 = 12 Hours
}
Função setMinutes
Enquanto a função setHours() ilumina apenas um LED para a hora atual, a função setMinutes() ilumina todos os LEDs até ao minuto atual. Por isso temos o ciclo for lá dentro.
void setMinutes(int m) {
uint32_t minColor = pixels.Color(100, 100, 0);
for (int i = 0; i <= m; i++) {
setPixel(i, minColor);
}
}
Função setSeconds
Por fim, queremos mostrar o segundo atual. Não quis mostrar o segundo por cima das horas e minutos já mostrados e, em vez disso, optei por tornar o LED do segundo atual um pouco mais brilhante – seja qual for a cor que esteja a mostrar.
void setSeconds(int s) {
uint32_t color = getPixel(s);
setPixel(s, color + 0x373737);
}
A função primeiro obtém a cor do LED no segundo atual s chamando getPixel() . Depois torna a cor mais brilhante adicionando 55 a cada valor de cor (vermelho, verde, azul). 55 em hexadecimal é 0x37 . É daí que vem este valor de 0x373737 . E é por isso que a cor base não pode ser maior que 200 , pois 200 + 55 = 255 , que é o valor máximo de cor.
Função showTime
Para mostrar uma hora ( h, m, s ) no relógio, basta chamar as funções acima pela seguinte ordem e atualizar o estado dos LEDs via pixels.show() no fim.
void showTime(int h, int m, int s) {
pixels.clear();
setMinutes(m);
setTicks(m);
setHours(h);
setSeconds(s);
pixels.show();
}
Função simulateClock
Para testar a visualização da hora, estou a usar um relógio simulado simples que percorre as horas, minutos e segundos 10x mais 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);
}
}
}
}
Funções setup e loop
As funções setup e loop agora são muito simples. Na função setup iniciamos o anel de LED. E na função loop simulamos o funcionamento do relógio.
void setup() {
pixels.begin();
pixels.clear();
pixels.setBrightness(10);
pixels.show();
}
void loop() {
simulateClock();
}
Tirando mostrar a hora real, agora temos tudo para mostrar a hora no nosso anel de LED. Podes usar este código de simulação para encontrar as cores e visualizações que preferes, sem te preocupares com a hora real a mostrar.
Para mais informações sobre os LEDs WS2812 e os efeitos que podes conseguir, vê o nosso tutorial How To Control WS2812B Individually Addressable LEDs using Arduino . Nota, no entanto, que em maio de 2024, não consegui pôr a FastLED library , que é usada neste tutorial, a funcionar com um ESP32.
Na próxima secção vamos buscar a hora real a um fornecedor de hora na internet e usar o código acima para a mostrar no nosso relógio.
Código para um Relógio de LED Ring com Hora da Internet
Podíamos usar o relógio interno do ESP32 para obter o sinal de hora. Mas isso significa que sempre que o relógio perde energia temos de acertar a hora. Também teríamos de ajustar a hora duas vezes por ano quando muda o horário de verão.
O que significa que precisaríamos de botões ou software extra (interface web, app de telemóvel) para poder acertar a hora. Isto é tudo um pouco chato. Prefiro ter um relógio totalmente automático, obtendo sempre a hora certa de um fornecedor de hora na internet.
Se quiseres saber como isso funciona em detalhe, vê o nosso tutorial Automatic Daylight Savings Time Clock . Basicamente copiei e colei o código de lá para o código abaixo.
#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 poderes usar este código, vais precisar de instalar duas bibliotecas adicionais, nomeadamente ” ArduinoJson.h ” e ” TimeLib.h “. ” WiFi.h ” e ” HTTPClient.h ” fazem parte da biblioteca standard ESP32/Arduino e não precisam de ser instaladas à parte.
Também vais precisar de definir o SSID e a password da tua rede WiFi. Isso vai permitir ao ESP32 ligar-se ao fornecedor de hora na internet no URL indicado:
#define WIFI_SSID "YOUR_SSID" #define WIFI_PASSPHRASE "YOUR_PASSWORD" #define URL "http://worldtimeapi.org/api/ip"
Com isso resolvido, vamos ver mais de perto as novas funções adicionadas ao código.
Função syncTime
A função syncTime() liga-se ao fornecedor de hora na internet, lê os dados em formato JSON, extrai a informação relevante da hora (h, m, s, D, M, Y) e acerta o relógio interno do ESP32 em conformidade.
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();
}
Função shouldSyncTime
Não queremos consultar o fornecedor de hora na internet demasiadas vezes e correr o risco de ser bloqueados. Por isso, limito a sincronização do relógio interno do ESP32 com o fornecedor de hora na internet a uma vez por hora. Especificamente, sincronizamos ao 3º segundo de cada hora. A função shouldSyncTime() diz-nos quando é altura disso.
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;
}
Função updateDisplay
A função updateDisplay() substitui a função simulateClock() que usámos antes. Lê o relógio interno do ESP32, que sincronizámos com a hora da internet, e chama showTime() para a mostrar no anel de LED.
void updateDisplay() {
time_t t = now();
showTime(hourFormat12(t), minute(t), second(t));
}
Função setup
Na função setup() adicionamos a funcionalidade de ligar à rede WiFi, mas de resto inicializamos o anel de LED da mesma forma 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();
}
Função loop
Na função loop() primeiro verificamos se devemos sincronizar a hora. Se for o caso, chamamos syncTime() para isso. Mas em qualquer caso, chamamos updateDisplay() para mostrar a hora atual no anel de LED.
void loop() {
if (shouldSyncTime())
syncTime();
updateDisplay();
delay(100);
}
Como temos um delay(100) no loop, esta atualização acontece a cada 100ms – ou 10 vezes por segundo.
E é isso! Assim tens um relógio totalmente automático, sempre certo, que mostra a hora num anel de LED. Nas próximas secções vamos adicionar regulação automática do brilho e ativação por movimento ao relógio para o tornar ainda melhor.
Regulação Automática do Brilho do Relógio de LED Ring
Os LEDs WS2812 são muito brilhantes. Normalmente não os queres tão brilhantes à noite. Por outro lado, se ajustares o brilho para ser adequado a um quarto escuro, o relógio será difícil de ler durante o dia. Seria ótimo ajustar automaticamente o brilho dos LEDs consoante as condições de luz atuais.
Para isso, vamos adicionar uma resistência sensível à luz (LDR) ao nosso circuito, ler o seu valor através de uma entrada analógica no ESP32 e chamar setBrightness() para ajustar o brilho geral do anel de LED.
Circuito do Relógio de LED Ring com LDR
A imagem abaixo mostra como ligar o sensor de luz (LDR) ao circuito existente. Um pino do LDR é ligado a 5V e o outro pino a um potenciómetro de 10KΩ, que por sua vez é ligado à massa.

A saída do LDR (fio verde) é ligada ao GPIO0 do ESP32. Qualquer outro pino GPIO também serve, desde que consiga ler um sinal analógico.
O LDR e o potenciómetro de 10KΩ formam um divisor de tensão. Para mais detalhes sobre como isto funciona, vê o nosso tutorial How to detect light using an Arduino , de onde este circuito foi retirado.
Dependendo do brilho da luz ambiente, o circuito do LDR vai produzir uma tensão proporcional no intervalo de 0V até 5V. Na prática, nunca se obtém o intervalo completo, pois nunca há escuridão total nem brilho máximo. O potenciómetro de 10KΩ permite definir um ponto de funcionamento para a tensão de saída. Vamos mapear as variações de tensão para um intervalo adequado no código abaixo.
Código de teste para ajustar o intervalo de brilho
Antes de adicionar a funcionalidade de regulação automática do brilho ao nosso Relógio de LED Ring, primeiro precisamos de ajustar os parâmetros. Dependendo da luz ambiente, queremos um valor de brilho entre 5, quando está muito escuro, e talvez 50, quando está claro.
O que lemos do sensor de luz na entrada analógica, no entanto, serão valores entre 0 e 4095. E isso vai depender da resistência padrão do LDR, da configuração do potenciómetro e das condições de luz ambiente.
O código de teste seguinte permite encontrar o mapeamento certo entre os valores do sensor LDR e os valores de brilho que queremos definir.
#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);
}
Lê o valor do LDR ( ldrValue ), imprime-o, mapeia-o para um valor de brilho ( brightness ) e imprime isso também. Se carregares e executares este código, deves ver algo assim no Serial Monitor.

Se tapares o sensor de luz (escuridão), queres ver um valor de brilho próximo de 5 impresso, e se expuseres o sensor a luz muito forte (por exemplo, luz solar), queres obter um valor de brilho próximo de 50. Para isso, terás de ajustar os parâmetros fromLow , fromHigh da função map:
map(value, fromLow, fromHigh, toLow, toHigh)
Para o meu sensor LDR, configuração do potenciómetro e condições de luz, acabei com fromLow=1400 e fromHigh=3000 , como podes ver no código acima. Os teus valores serão diferentes, mas podes usar os meus como ponto de partida.
Da mesma forma, se quiseres um intervalo de brilho diferente de 5..50 , podes escolher outros valores para toLow e toHigh . Podes até desligar o relógio à noite escolhendo toLow=0 .
Adicionar código de regulação automática do brilho
Depois de encontrares bons parâmetros, podes adicionar a seguinte função de regulação automática do brilho ao código do relógio.
void updateBrightness() {
if (millis() % 1000 < 100) {
int ldrValue = analogRead(LDRPIN);
int brightness = map(ldrValue, 1400, 3000, 5, 50);
pixels.setBrightness(constrain(brightness, 5, 50));
}
}
Define o brilho de todo o anel de LED com base no valor lido em LDRPIN . No entanto, não queremos ajustar o brilho sempre que atualizamos o relógio, o que acontece a cada 100ms. Isso poderia causar um efeito de cintilação irritante, pois qualquer pequena alteração na luz ambiente pode mudar o brilho dos LEDs.
Por isso, só atualizamos o brilho a cada segundo, o que é conseguido por millis() % 1000 < 100. Se quiseres menos ou mais atualizações do que a cada 1000ms, basta mudar o 1000 para o que preferires. Nota que o limiar de < 100 está ligado ao atraso de 100ms no loop principal.
Nota também a chamada extra a onstrain(brightness, 5, 50), que garante que o brilho está no intervalo 5..50 , o que não é garantido pela função map() .
Para integrar a regulação do brilho no código do relógio, basicamente só precisamos de adicionar uma chamada à função updateBrightness() no loop principal:
void loop() {
if (shouldSyncTime())
syncTime();
updateBrightness();
updateDisplay();
delay(100);
}
Claro que também há a definição da constante LDRPIN e a configuração do LDRPIN como entrada. O código completo para o Relógio de LED Ring com regulação automática do brilho está abaixo:
#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);
}
Ufa, estamos quase a terminar. Como último melhoramento, vamos ligar o relógio automaticamente se for detetado movimento.
Ativação Automática do Relógio de LED Ring
O Relógio de LED Ring consome bastante energia e há o risco de queimar os LEDs se o relógio estiver sempre ligado. Mas não faz sentido mostrar a hora se ninguém está lá para a ver. Por isso, vamos adicionar um sensor de movimento (PIR) ao circuito. Ele vai ligar o relógio durante um período definido (por exemplo, 10 segundos) se for detetado movimento e depois desliga o anel de LED.
A imagem seguinte mostra como adicionar o sensor de movimento PIR ao circuito existente. Basta ligar 5V e GND aos pinos correspondentes do sensor PIR (fios vermelho e azul). A saída do sensor (fio roxo) é ligada ao GPIO10 do ESP32.

Aqui está uma foto do circuito completo numa breadboard. Como podes ver, consegui mesmo correr o Relógio de LED Ring para testes com uma pilha de 9V (com um regulador de 5V).

No entanto, se quiseres mesmo correr o relógio a bateria, recomendo usar um powerbank USB. Também podes querer pôr o ESP32 em deep-sleep enquanto o relógio está inativo. Para mais detalhes sobre isso, vê o nosso tutorial sobre How to Build a Motion Activated Night Light .
Antes de integrares a ativação automática no código do relógio, vamos primeiro escrever um código de teste para o sensor PIR.
Código de teste para o sensor PIR
O código seguinte lê o sinal do sensor PIR e imprime “motion detected” se for detetado movimento ou “nothing” caso contrário.
#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);
}
O sensor de movimento que estou a usar aqui é o AM312 , que vai emitir um sinal alto durante apenas cerca de 2 segundos após o primeiro movimento detetado. Para mais detalhes sobre o AM312 e como o usar, vê o nosso tutorial How to Build a Motion Activated Night Light .
Se usarmos o sinal do AM312 diretamente no nosso código, o relógio seria ativado apenas durante 2 segundos e depois desligava-se se não houvesse mais movimento. Isso resulta num ligar/desligar frequente, o que não é agradável. O código acima, por isso, adiciona um temporizador que faz com que motionDetected() retorne true durante pelo menos 10000ms = 10 segundos. Podes aumentar ou diminuir esse tempo como quiseres.
Se montares o circuito, carregares e executares o código, e funcionar como esperado, estás pronto para integrar o código de ativação automática no código do relógio.
Adicionar código de ativação automática
Para a integração, vamos adicionar duas funções e também mudar um pouco o loop principal.
Função motionDetected
A função motionDetected() baseia-se no código de teste acima e retorna true durante pelo menos 10 segundos após o primeiro movimento detetado. O temporizador de 10 segundos é reiniciado sempre que é detetado novo movimento durante esse período. Assim, o relógio mantém-se sempre ligado enquanto alguém estiver por perto e em movimento.
bool motionDetected() {
if (digitalRead(PIRPIN))
lastMotion = millis();
unsigned long diff = millis() - lastMotion;
return diff < 10000 && diff >= 0;
}
Nota a condição diff >= 0 na instrução de retorno. É necessária, pois o valor do temporizador devolvido por millis() vai wrap ao fim de alguns dias (49 num Arduino UNO) e a diferença de tempo diff seria negativa.
Função clearDisplay
Se não for detetado movimento, desligamos os LEDs do relógio chamando clearDisplay() . Esta função simplesmente limpa todos os valores de pixel definidos e depois atualiza o estado dos LEDs chamando show() .
void clearDisplay() {
pixels.clear();
pixels.show();
}
Função loop
Por fim, precisamos de alterar a função loop para reagir ao sensor de movimento. Se detetarmos movimento via motionDetected() , executamos a atualização típica do relógio. Se não, desligamos o mostrador do relógio via clearDisplay() . Tudo isto acontece a cada 1/10 de segundo devido ao delay(100) .
void loop() {
if (motionDetected()) {
if (shouldSyncTime())
syncTime();
updateBrightness();
updateDisplay();
} else {
clearDisplay();
}
delay(100);
}
E é isso. Com isto temos todas as peças necessárias. Abaixo está o código completo para o relógio ativado por movimento.
Código completo para relógio ativado por movimento
#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);
}
Conclusões
Neste tutorial aprendeste a construir um Relógio de LED Ring ativado por movimento, com regulação automática do brilho e sincronização da hora via internet. Tem a vantagem de ser sempre preciso e totalmente automático. Não precisas de botões ou manípulos para o ligar ou desligar, acertar a hora ou ajustar o brilho.
Além de construir uma estrutura de relógio melhor e mudar as cores dos LEDs ao teu gosto, há mais algumas melhorias que podes adicionar. Podes juntar um sensor de temperatura que ajusta a cor dos LEDs para indicar a temperatura ambiente. Por exemplo, mais azulado quando está frio e mais avermelhado quando está quente.
Como o relógio é ativado por movimento, seria possível usá-lo a bateria, especialmente se colocares o ESP32 em deep-sleep enquanto o mostrador está inativo. Vê o nosso tutorial sobre How to Build a Motion Activated Night Light como exemplo.
Finalmente, também podias alternar a cada poucos segundos entre mostrar a hora e mostrar algum tipo de representação da data atual, já que já estamos a descarregar a informação da data do fornecedor de hora na internet. Em vez do worldtimeapi.org, também podes usar um servidor SNTP que permite sincronizar a hora mais frequentemente. Vê o nosso tutorial sobre How to synchronize ESP32 clock with SNTP server .
E se não gostares nada do Relógio de LED Ring, vê este tutorial Digital Clock with CrowPanel 3.5″ ESP32 Display , que explica como mostrar a hora e data num ecrã bonito, tal como um relógio digital convencional.
Há muito para explorar!
Se tiveres dúvidas, sente-te à vontade para perguntar. Terei todo o gosto em ajudar ; )

