Skip to Content

LED Ring Clock with WS2812

LED Ring Clock with WS2812

In this tutorial you will learn how to build an LED Ring clock with the WS2812 LED strip and an ESP32. We will add automatic dimming to the clock and will also use an internet time provider to keep it always accurate. Finally, the clock will be motion activated, using a PIR sensor, to not waste unnecessary energy.

The short clip below shows the clock in action. You can see hour marks (white dots) and the seconds running (moving white dot).

LED Ring Clock in action
LED Ring Clock in action

The current hour is marked by the orange dot and the minutes by the yellow dots. So, in the picture above the clock shows the time 11:36.

By synchronizing the clock over Wi-Fi with an internet time provider, we can ensure that our clock always displays the correct time, regardless of changes in daylight saving time, intermittent power losses, or due to an inaccurate internal clock.

Let’s start with the required parts.

Required Parts

Here are the required parts for the project. Instead of the ESP32-C3 Mini Development Board listed below, I used very similar board called ESP32-C3 SuperMini from AliExpress. Mine had only a single color built-in LED, while the board below has an RGB LED. But apart form that they should be almost identical and both should work. Any other ESP32 or ESP8266 will be fine as well. However, if you want to use an Arduino you will need Wi-Fi shield.

ESP32-C3 Mini

USB C Cable

RGB LED Ring

Dupont wire set

Dupont Wire Set

Half_breadboard56a

Breadboard

Resistor & LED kit

Variable Resistor Set

Motion Sensor

Photoresistor set

Makerguides.com is a participant in the Amazon Services LLC Associates Program, an affiliate advertising program designed to provide a means for sites to earn advertising fees by advertising and linking to products on Amazon.com. As an Amazon Associate we earn from qualifying purchases.

Basis of the WS2812 RGB LED Strip

We are going to use a WS28112 RGB LED ring for our clock. The WS2812 is a specific type of RGB LED that is based on the 5050 LED package size. The 5050 RGB LED refers to the dimensions of the LED package, which is 5.0mm x 5.0mm. The picture below shows a 5050 LED with its control IC and the three LEDS (green, red, blue).

5050 RBG LED
5050 RBG LED (source)

Note that there are various other similar RGB LED Strip Drivers such as the WS2811 SK6812 and WS2815. This quick guide contains a nice comparison of the different types. Here we are going to use the WS2812.

The WS2812 operates at 5V, has a built-in PWM controller for color mixing, and can be daisy-chained, using a single communication line to create longer LED strips. Most commonly you will find the WS2812 used in flexible RGB LED Strips as the one shown below.

WS2812 RGB LED Strip
WS2812 RGB LED Strip (source)

These LED Strips come with various numbers of LEDs and densities and can be cut to length. In addition, to flexible strips you can also get them in the shape of rigid rings:

WS2812 RGB LED Rings
WS2812 RGB LED Rings (source)

The smaller rings come in one piece, but the big ring with 60 LEDs we are going to use for our clock, comes in four parts that you will need to solder together. The picture below shows the backside of the four pieces:

WS2812 RGB LED Ring in four parts
WS2812 RGB LED Ring in four parts

How to assemble the four segments in one big ring is the topic of the next section.

Constructing the LED Ring Clock

Since an hour has 60 minutes and a minute 60 seconds, we want to use a ring with 60 LEDs. As mentioned above, due to its size it comes in 4 sections (each with 15 LEDs) that we need to solder together.

Note that WS2812 RGB LED Strips and Rings have an input (DIN) and an output DO) side as shown in the image below:

Input and Output of an WS2812 LED Strip
Input and Output of an WS2812 LED Strip

Start by soldering the signal wire (yellow) to the DIN pad of one of the LED Ring segments. Next solder a ground wire (black) to the GND pad and a power supply wire (red) to the 5V pad. It should look like this:

Wiring of WS2812 LED Ring

Finally, we need to connect the four segments. Connect GND to GND, 5V to 5V and DOUT to DIN four each of the segments. A connection between segments should look like this.

Wiring of LED Ring Segments
Wiring of LED Ring Segments

You essentially can’t connect the pads wrongly, otherwise you wouldn’t get a nice ring:

Wiring of complete LED Ring
Wiring of complete LED Ring

Note that long LED Strips will suffer from a voltage drop that causes the LEDs at the end of the Strip to be less bright than the ones at the start. Sometimes you therefore find LED Strips with power supplies to both ends.

With the 60 LED Ring, I did not notice any difference in brightness between the first and the last LED. However, if you do, you can connect the GND and 5V pads between the last and first LED segment. I didn’t find this necessary in my case.

Also you may found the soldering a bit difficult since the pads are very small. It helps to place the LED Ring segments in a holder to fix them in place. I used a 3D printer to create the holder or clock frame shown in the next section.

LED Clock Frame

The LED Clock Frame consists of three parts. The back, where the LED Ring is located, a transparent front and a foot that allows me the stand up the LED Ring.

Parts of the LED Clock Frame
Parts of the LED Clock Frame

The complete LED Clock Frame looks like this and you can find the STL files here:

LED Clock Frame
LED Clock Frame

The next thing to do, is to connect the LED Ring to the ESP32.

Wiring the LED Ring Clock

Connecting the WS2812 LED Ring to the ESP32 is simple. First connect the GND of the ESP32 to the GND of the LED Ring (blue wire). Then connect the 5V from the ESP32 to the 5V input of the WS2812 LED Ring. And finally connect GPIO3 of the ESP32 via a 220Ω resistor to DIN of the WS2812.

Connecting ESP32 to WS2812 LED Ring
Connecting ESP32 to WS2812 LED Ring

You can get away without the 220Ω resistor to try things out but it is safer to have it. It protects the GPIO of the ESP32 from over currents. Instead of GPIO3 you can use any other GPIO pin as long as it supports PWM.

Note that the LED Ring can draw a lot of current! A single RGB LED is composed of three LEDs (red, green, blue) and each of them consumes up to 20mA, when fully on. Which means, if a single RGB LED is fully activated (white light) in draws up to 60mA (I measured 45mA). Multiply 60mA by 60 LEDs and we get a current of 3.6A, if all LEDs of the Ring are fully switched on!

That is typically too much current to draw from the ESP32 or the USB port. The test code in the next section therefore sets the brightness of the LED Ring to a low value of 10, which is plenty bright for testing.

Test Code for the LED Ring

Before trying to implement anything complex, we better test the function of the LED Ring with some simple code first. The code below sequentially switches all 60 LEDs on and when all LEDs are on, switches them off again. The allows us to check that all LEDs are working and that the ring segments are correctly wired.

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

In the code snippet above, we are using the Adafruit NeoPixel library. If you haven’t already, you will need to install the library first, before you can use this code.

Let’s break down the test code to understand its functionality in more detail.

Constants and Variables

First we define the constant DINPIN which specifies the data input pin to which the LED Ring is connected, and NUMPIXELS which defines the total number of Pixels/LEDs in the strip. In our case we have 60 LEDs and use GPIO 3.

#define DINPIN 3
#define NUMPIXELS 60

Setup function

In the setup() function, we initialize the LED Ring strip by calling pixels.begin(), clear any existing pixel colors with pixels.clear(), set the brightness level to 10 with pixels.setBrightness(10), and finally show the state of the pixels using pixels.show().

void setup() {
  pixels.begin();
  pixels.clear();
  pixels.setBrightness(10);
  pixels.show();
}

As I mentioned before, be careful when changing the brightness! At full brightness the LED Ring might draw too much current for your ESP32, USB port or whatever power supply you are using.

Loop function

The loop() function contains a loop that first turns all the LEDs white by iterating through each pixel using a for loop. The pixels.setPixelColor() function is used to set the color of each pixel to white (255, 255, 255) and then pixels.show() is called to show the LED states. A delay of 200ms is added between each pixel update.

After all the pixels are set to white, another loop is used to turn off all the LEDs by setting their color to black (0, 0, 0) and updating the strip with pixels.show() again with a delay of 200ms between each update.

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

If that works, then congratulations! In the next section, we try something a little bit more complex by simulating the time for the LED Ring to display and also improve our circuit a bit.

Improved Wiring of the LED Ring Clock

As mentioned above the LED Ring can draw up to 3.6A, when all LEDs of the Ring are fully switched on. We don’t want to draw that much current from the ESP32. Instead we need to connect the LED Ring to an external power supply. The circuit below shows how that is done:

Connecting WS2812 LED Ring to external power supply
Connecting WS2812 LED Ring to external power supply

The WS2812 runs on 5V, so you will need a 5V power supply with enough current for the LED Ring at your maximum brightness setting. At a brightness of 5, I measured a current of 80mA and at a brightness of 50 the current was 400mA.

The low brightness of 5 is good in dark room and the brightness of 50 is plenty in a bright room. With higher brightness values the clock can illuminate a room, which is not the purpose of a clock. So, in my case a 500mA power supply would be sufficient.

In the circuit above, I also added a recommended capacitor of 100μ up to 1000μ on the power supply line. This is to stabilize the power supply in case of current fluctuations caused by switching many LEDs simultaneously on or off.

In the next section, we move from the test code to a simulated clock that allows us to try out different ways and colors to display time on an LED Ring.

Code for a Simulated LED Ring Clock

There are many different ways you could display time on an LED Ring. The picture below shows the version I went with:

Showing time on an LED Ring
Showing time on an LED Ring

The current hour (orange dot) is marked by a single LED a the corresponding clock position. Minutes (yellow dots) are displayed by illuminating all LEDs up to the current minute. In the picture above the time is 11:32 and therefore the orange LED at the 11 o’clock position is on, and the 32 yellow LEDs showing the 32 minutes.

The ticks of the clock face are indicated by weakly white LEDs and the current second is shown by brightening the LED at the current second position. It is a bit hard too see in the picture above but the LED at the 3 second clock position is slightly brighter. How that is done, is shown in the following code.

Note that this code is not showing actual time but just simulates an accelerated time to test the display of time on the LED Ring. Have a quick look at the complete code first before we discuss its details.

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

In the code above, we are using the Adafruit NeoPixel library to simulate a clock using an LED strip with 60 pixels. The clock will display the hours, minutes, and seconds by changing the colors of specific pixels on the LED strip.

Constants and Variables

As before, first we define the constants and variables needed for the clock simulation. We specify the data pin for the LED strip, the total number of pixels, and an offset value for indexing the pixels.

#include "Adafruit_NeoPixel.h"

#define DINPIN 3
#define NPIXELS 60
#define OFFSET 29

Adafruit_NeoPixel pixels(NPIXELS, DINPIN, NEO_GRB + NEO_KHZ800);

Let’s talk a bit about the OFFSET constant and why it is needed. If you assemble the LED Ring Clock in its frame, then the first LED of the Ring (where the wires are connected to), will be located one pixel to the left of the 6 o’clock position (index 0).

Offset for pixel index
Offset for pixel index

So, if we have a time of 12 ‘clock (= 0 o’clock) we cannot switch on the LED/pixel at index position 0, because that is at the bottom. Instead we have to switch on the LED at index 29, since that is the 12 o’clock position. That means we always need to add an offset of 29 to any pixel index, if we want to switch on the LED at the corresponding clock position.

That is what the OFFSET constant is for. Depending on your clock frame and the mechanical position of the first LED of the ring, you may need to use a different OFFSET.

Index function

The OFFSET constant is used in the index() function to map a time index i to an LED position on the LED Ring. In addition to the offset, we also need to calculate the modulo (%) to ensure that the LED index is in the range 0..59, since we have only 60 LEDs.

int index(int i) {
  return (i + OFFSET) % NPIXELS;
}

Let’s say we have a time of 36 minutes. The LED on the ring we need to illuminate to show the minute would be at index: 36 + 29 % 60 = 5.

setPixel and getPixel functions

The setPixel() and getPixel() functions use the index() function to set or get the color for a given index i on the LED Ring:

void setPixel(int i, uint32_t color) {
  pixels.setPixelColor(index(i), color);
}

uint32_t getPixel(int i) {
  return pixels.getPixelColor(index(i));
}

setTick function

The setTick() function marks the hourly ticks on the LED ring. Since a day on a clock has 12 hours but we have 60 LEDs, we need to illuminate every 5-th LED on the ring to mark the hours.

void setTicks(int m) {
  uint32_t tickColor = pixels.Color(50, 50, 50);
  for (int h = 0; h < 12; h++) {
    setPixel(h * 5, tickColor);
  }
}

If you look at the function; it iterates over the 12 hours, multiplies an hour h by 5 and then uses setPixel() and therefore index() to convert the hour to an LED index, where the color is set.

I am using a week white color (50, 50, 50) but you can pick any color you like. Just make sure the color value is not greater than 200, due to the way seconds are displayed. More about that later.

setHours function

The setHours() function works just like the setTicks() function, but displays a specific hour instead of all hours and uses a different color. I picked a warm orange-red color for the hour mark.

void setHours(int h) {
  uint32_t hourColor = pixels.Color(200, 50, 0);
  setPixel(h * 5, hourColor);  // h: 0..11 = 12 Hours
}

setMinutes function

While the setHours() function illuminates only a single the LED for the current hour, the setMinutes() function illuminates all LEDs up to the current minute. That’s why we have the for loop in there.

void setMinutes(int m) {
  uint32_t minColor = pixels.Color(100, 100, 0);
  for (int i = 0; i <= m; i++) {
    setPixel(i, minColor);
  }
}

setSeconds function

Finally, we want to show the current second. I did not want to display the second over the hours and minutes shown and instead opted for making the LED for the current second a bit brighter – whatever color it currently shows.

void setSeconds(int s) {
  uint32_t color = getPixel(s);
  setPixel(s, color + 0x373737);
}

The function first gets the LED color at the current second s by calling getPixel(). It then makes the color brighter by adding 55 to each color value (red, green, blue). 55 in hexadecimal is 0x37. That is where this value of 0x373737 comes from. And that is the reason, why the base color cannot be larger than 200, since 200 + 55 = 255, which is the maximum color value.

showTime function

To show a time (h, m, s) on the clock we simply call the functions above in the following sequence and update the LED states via pixels.show() at the end.

void showTime(int h, int m, int s) {
  pixels.clear();
  setMinutes(m);
  setTicks(m);  
  setHours(h);
  setSeconds(s);
  pixels.show();
}

simulateClock function

To test the display of time, I am using a simple simulated clock that runs through the hours, minutes and seconds 10x faster (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);
      }
    }
  }
}

setup and loop function

The setup and loop function are now very simple. In the setup function we initiate the LED Ring. And in the loop function we simulate the running of the clock.

void setup() {
  pixels.begin();
  pixels.clear();
  pixels.setBrightness(10);
  pixels.show();
}

void loop() {
  simulateClock();
}

Apart from showing the actual time we now have everything to display the time on our LED Ring. You can use this simulator code to find the colors and visualizations you like, without having to worry about the actual time to show.

For more information on the WS2812 LEDs and the kind of affects you can achieve have a look at our tutorial How To Control WS2812B Individually Addressable LEDs using Arduino. Note, however, that of May 2024, I could not get the FastLED library, which is used in this tutorial to work with an ESP32.

In the next section we get the actual time from an internet time provider and use the code above to display it on our clock.

Code for a Web Time LED Ring Clock

We could use the real time clock of the ESP32 to get a time signal. But that means every time the clock looses power we have to set the current time. Also we would have to adjust the time twice a year when the daylight saving time switch occurs.

Which means we would need to have buttons or extra software (web interface, phone app) to be able to set the time. This is all rather cumbersome. Instead I prefer to have a fully automated clock by getting an always accurate time from an internet time provider.

If you want to learn how that works in detail have a look at our tutorial Automatic Daylight Savings Time Clock. I essentially copy & pasted the code from there into the code below.

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

Before you can use this code you will need to install two additional libraries, namely “ArduinoJson.h” and “TimeLib.h“. “WiFi.h” and “HTTPClient.h” are part of the ESP32/Arduino standard library and don’t need to be installed separately.

You will also need to set the SSID and password of your WiFi network. It will allow the ESP32 to connect to the internet time provider at the given URL:

#define WIFI_SSID "YOUR_SSID"
#define WIFI_PASSPHRASE "YOUR_PASSWORD"
#define URL "http://worldtimeapi.org/api/ip"

With that out of the way, let’s have a closer look at the new functions added to the code.

syncTime function

The syncTime() function connects to the internet time provider, reads the data in JSON format, extracts the relevant time information (h, m, s, D, M, Y) and sets the internal clock of the ESP32 accordingly.

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

shouldSyncTime function

We don’t want to poll the internet time provider too often and potentially get blocked. I therefore limit the synchronization of the ESP32 internal clock with the internet time provider to once an hour. Specifically, we synchronize at the 3rd second after each hour. The shouldSyncTime() function tells us when it is time for that.

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

updateDisplay function

The updateDisplay() function replaces the simulateClock() function we used before. It reads the internal clock of the ESP32, which we have synchronized with the internet time, and calls showTime() to display it on the LED Ring.

void updateDisplay() {
  time_t t = now();
  showTime(hourFormat12(t), minute(t), second(t));
}

setup function

In the setup() function we add the functionality to connect to the WiFi network but otherwise initialize the LED Ring in the same way as before.

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

loop function

In the loop() function we first check if we should synchronize the time. If that is the case, we call syncTime() to do that. But in any case we call updateDisplay() to display the current time on the LED Ring.

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

Since we have a delay(100) in the loop this update happens every 100msec – or 10 times a second.

And that’s it! With that you have a fully automatic, always accurate clock that shows the time on an LED Ring. In the next sections we add automatic dimming and motion activation to the clock to make it even better.

Automatic Dimming of the LED Ring Clock

The WS2812 LESs are very bright. Usually you don’t want them that bright at night. On the other hand, if you adjust the brightness to be suitable for a dark room the clock will be hard to read during the day. It would be nice to automatically adjust the LED brightness based on the current lighting conditions.

For that purpose, we add a light sensitive resistor (LDR) to our circuit, read its value via an analog input on the ESP32 and call setBrightness() to adjust the overall brightness of the LED Ring.

LED Ring Clock circuit with LDR

The image below shows you how to connect the light sensor (LDR) to the existing circuit. One pin of the LDR is connected to 5V and the other pin to a 10KΩ potentiometer, which in turn is connected to ground.

Wiring of LDR with LED Ring Clock
Wiring of LDR with LED Ring Clock

The output of the LDR (green wire) is connected to GPIO0 of the ESP32. Any other GPIO pin will work as well as long as it can read an analog signal.

The LDR and the 10KΩ potentiometer building a voltage divider. For more details on how this works have a look at our tutorial How to detect light using an Arduino, where this circuit originates from.

Depending on the brightness of the ambient light, the LDR circuit will produce a proportional voltage in the range of 0V up to 5V. In practice, you never get the full range, since you will never have complete darkness and maximum brightness. The 10KΩ potentiometer allows you to set a working point for the output voltage. We will map the changes in voltage to a suitable range in the code below.

Test code to adjust brightness range

Before adding the auto dimming functionality to out LED Ring Clock, we first will need to adjust the parameters. Depending on the ambient light, we want a brightness value between 5, when it is very dark, and maybe 50, when it is bright.

What we are reading from the light sensor on the analog input, however, will be values in the range 0 to 4095. And that will depend on the default resistance of the LDR, the setting of the potentiometer and the ambient light conditions.

The following test code allows you to find the right mapping between the LDR sensor values and the brightness values we want to set.

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

It reads the LDR value (ldrValue), prints it, maps it to a brightness value (brightness) and prints that out as well. If you upload and run this code, you should see something like that on the Serial Monitor.

Serial output of test code to adjust brightness
Serial output of test code to adjust brightness

If you cover up the light sensor (darkness), you want to see a brightness value close to 5 printed out, and if you expose the sensor to very bright light (e.g. sunlight), you want to get a brightness value close to 50. To achieve that, you will have to tweak the fromLow, fromHigh parameters of the map function:

map(value, fromLow, fromHigh, toLow, toHigh)

For my LDR sensor, potentiometer setting, and light conditions, I ended up with fromLow=1400 and fromHigh=3000, as you can see in the code above. Your values will be different but you can use mine as a starting point.

Similarly, if you want a brightness range different to 5..50, you can pick different values for toLow and toHigh. You could even switch the clock off at night by picking toLow=0.

Adding automatic dimming code

Once you have found nice parameter settings, you can add the following automatic dimming function to the clock code.

void updateBrightness() {
  if (millis() % 1000 < 100) {
    int ldrValue = analogRead(LDRPIN);
    int brightness = map(ldrValue, 1400, 3000, 5, 50);
    pixels.setBrightness(constrain(brightness, 5, 50));
  }
}

It sets the brightness of the entire LED Ring based on the value we read at the LDRPIN. However, we don’t want to adjust the brightness every time we update the clock, which happens every 100ms. This could cause an annoying flickering effect, since any small change in ambient light might change the LED brightness.

We therefore update the brightness only every second, which is achieved by millis() % 1000 < 100. If you want to have less or more frequent updates than every 1000ms, just change the 1000 to something you prefer. Note that the threshold of < 100 is coupled to the 100ms delay in the main loop.

Also note the extra call to constrain(brightness, 5, 50), which ensure that the brightness is in the range 5..50, which is not guaranteed by the map() function.

To integrate the dimming in the clock code, we essentially just need to add a call to the updateBrightness() function to the main loop:

void loop() {
  if (shouldSyncTime())
    syncTime();
  updateBrightness();  
  updateDisplay();
  delay(100);
}

Of course, there is also the definition of the LDRPIN constant, and the setting of LDRPIN as input. The complete code for the LED Ring Clock with automatic dimming is shown below:

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

Phew, We are almost done. As a last refinement, we automatically switch the clock on, if motion is detected.

Automatic Activation of the LED Ring Clock

The LED Ring Clock consumes quite a bit of power and there is the risk of burn-out of the LEDs if the clock runs all the time. But there is no point in showing the time if nobody is there to actually see it. We therefore going to add a motion sensor (PIR) to the circuit. It will switch on the clock for a specified period (e.g. 10 seconds) if motion was detected and after that switches the LED Ring off.

The following image shows how to add the PIR motion sensor to the existing circuit. Just connect 5V and ground to the corresponding pins of the PIR sensor (red and blue wires). The output of the sensor (purple wire) is connected to GPIO10 of the ESP32.

Wiring of PIR sensor with LED Ring Clock
Wiring of PIR sensor with LED Ring Clock

Here is a picture of the completed circuit on a breadboard. As you can see, I was actually able to run the LED Ring Clock for testing purposes on a 9V battery (with a 5V voltage regulator).

LED Ring Clock with PIR sensor and Battery
LED Ring Clock with PIR sensor and Battery

However, if you really want to run the clock on battery power I recommend using a USB power pack instead. You also may want to put the ESP32 into deep-sleep, while the clock is inactive. For more details on that have a look at our tutorial on How to Build a Motion Activated Night Light.

Before integrating the automatic activation into the clock code, let’s write some test code for the PIR sensor first.

Test code for PIR sensor

The following code reads the signal from the PIR sensor and prints “motion detected” if motion was detected or “nothing” otherwise.

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

The motion sensor, I am using here, is the AM312, which will output a high signal for only about 2 seconds after the first movement was detected. For more details on the AM312 and how to use it have a look at our tutorial How to Build a Motion Activated Night Light.

If we use the signal of the AM312 directly in our code, our clock would be activated for only 2 seconds and then would switch off again if no further movement occurs. That results in a frequent on/off switching, which doesn’t look very nice. The code above therefore adds a timer component that lets motionDetected() return true for at least 10000msec = 10 seconds. You can lengthen or shorten that time period to your liking.

If you build the circuit, upload and run the code, and it works as intended, you are ready to integrate the automatic activation code into the clock code.

Adding automatic activation code

For the integration, we are going to add two functions and also will change the main loop a tiny bit.

motionDetected function

The motionDetected() function is based on test code above and returns true for at least 10 seconds after the first movement was detected. The 10 second timer resets every time new motion is detected during this period. That keeps the clock consistently on, as long as someone is around and moving.

bool motionDetected() {
  if (digitalRead(PIRPIN))
    lastMotion = millis();
  unsigned long diff = millis() - lastMotion;
  return diff < 10000 && diff >= 0;
}

Note the diff >= 0 condition in the return statement. It is needed, since the timer value returned by millis() will wrap after a number of days (49 for an Arduino UNO) and the time difference diff would be negative.

clearDisplay function

If no motion was detected, we switch of the clock LEDs by calling clearDisplay(). This function simply clears all set pixel values and then updates the LED states by calling show().

void clearDisplay() {
  pixels.clear();
  pixels.show();
}

loop function

Finally, we need to change the loop function to react to the motion sensor. If we detect motion via motionDetected(), we execute our typical update of the clock. If not, we switch of the clock display via clearDisplay(). All of that happens every 1/10 of a second due to the delay(100).

void loop() {
  if (motionDetected()) {
    if (shouldSyncTime())
      syncTime();
    updateBrightness();
    updateDisplay();
  } else {
    clearDisplay();
  }
  delay(100);
}

And that’s it. With that we have all the pieces needed. Below is the complete code for the motion activated clock.

Complete code for motion activated clock

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

Conclusions

In this tutorial you learnt how to build a motion activated, LED Ring Clock with automatic dimming and internet time synchronization. It has the advantage that it is always accurate and fully automatic. You don’t need any buttons or knobs to switch it on or off, to set the time or to adjust the brightness.

Apart from building a better clock frame and changing the LED colors to your liking, there are a few more improvements you could add. A temperature sensor could be added that would adjust the color of the LEDs to indicate the ambient temperature. For instance, more blueish when it is cold and more reddish when it is hot.

Since the clock is motion activated, it would be possible to run it on battery power, especially when putting the ESP32 into deep-sleep, while the display is inactive. Have a look at our tutorial on How to Build a Motion Activated Night Light as an example.

Finally, you could also switch every view seconds from showing the time to showing some kind of representation of the current date, since we are already downloading the date information from the internet time provider as well.

And if you don’t like the Ring Clock at all, have a look at this tutorial Digital Clock with CrowPanel 3.5″ ESP32 Display, that explains how to show time and date on a nice display, just like a conventional Digital Clock.

Plenty of things to play with!

If you have any questions feel free to post them. Happy to help ; )