Introduction: Decorative Analog LED Strip IoT Clock

An analog IoT LED clock running on an ESP8266, driving a WS2812B LED strip, constantly synched using an NTP server and controlled by your smartphone via a web server 😅.

Actually, It is not that complicated and all of the above is really easy to achieve if you already have some experience with Arduino, ESP, LED strips, and basic coding.

I wanted to make this clock for a long time ago, and I even found some projects here, but none of them had all the key features I visioned in mind, and the way I see it, It had to have all of these properties:

  • Decorative - a clock I would want to hang in my living room.
  • Analog - we already have millions of DIY digital clocks.
  • Light projection - outwards and inwards.
  • Easy to build - Snap fit, no screws, minimum joints, and easy to construct.
  • No RTC - yet super accurate.
  • IoT - for connectivity, settings, time sync, and control.

The hands of the clock are animated on the LED strip by visualizing the length of each hand:

Second hand (Red) - Long, outward + inward projection

Minute hand (Green) - Long, outward projection only

Hour hand (Blue) - Short, inward projection only

Supplies

  1. ESP8266/ESP32 (I've used this one: V3 Nodemcu-CH340)
  2. WS2812B LED strip, at least 2 meters or 120 LEDs in total - (100 per meter, model: WS2812BECO White PCB 2m 100 IP30)
  3. Wires
  4. Soldering iron
  5. 3D printer*
  6. Laser cutter*
  7. Multimeter
  8. Basic tools (pliers, cutters...)

*You only need a one-time access to a 3D printer and a Laser cutter, I used my own Ender 3 for printing, but for the cutting, I used a laser cutter at a local community makers lab.

Step 1: Design Iterations

Basically, this is not a step you will have to go through (you can skip this one ), yet the design process really affected the outcome and I wanted to share it.

I designed the clock using Autodesk Fusion 360 which I highly recommend, It took a couple of days to achieve a print-ready model. The entire design was made for 100 LEDs per meter strip, but I used parametric modeling in Fusion 360 so I could easily scale this project to fit longer strips for bigger clocks. I also had to split it into 4 in order to fit my printing bed (that's also a design feature for scaling up, but if you have a bigger printing volume you can combine the parts and print all at once).

The print was awesome, it snap-fitted, all the parts were held together by the LED strip itself and it actually worked! but I didn't like the outcome 💩, it felt like a prototype instead of a real product, also the light diffusion was awful, and the light was leaking from the front.

I decided to use wood instead of plastic, and I needed some cutting layouts, I went back to Fusion 360 and since everything was parametric, I quickly produced the cutting instructions for the laser cutter, based on the design I already made, consisting of 3 rings: 1 wood cover and 2 acrylic ones for holding the strip.

This time it was beautiful 🌈, the wood looked professional, and since it had no diffusion it made the colors blend perfectly, but after I tested it I realized the acrylic is too heavy and served no functional purpose other than holding the LED strip which didn't fit easily inside, and it was an expensive material. At that moment I decided to combine the two prototypes, I manually removed the diffusion walls from the 3D print, fit the strip inside, and covered it with the wooden ring.

I returned to my model one last time to remove the diffusion walls from the original print so you won't waste material and print time by having to break the diffusion walls after printing.

Step 2: 3D Print the Base

Use the 2 parts attached and print 2 of each.

Keep in mind that they are not the same, and you have to arrange them in ABAB formation.

Use your preferred settings and filament, no one will ever see the base anyways.

(You don't have to break your print, I already removed the diffusion walls from the final STL)

Step 3: Laser Cut the Wood Cover

Use the attached SVG for cutting the wood cover.

The red vector is for cutting and the black vector is for engraving, marking the center of the base.

I used 4mm Poplar plywood but you can use any material you like.

Sand any burn marks from the laser cutting process.

Step 4: Prepare the LED Strip

Cut a 120-centimeter-long strip with 120 LEDs on it.

Fold it in half making sure each LED sits perfectly behind another LED.

Carefully remove the adhesive protection, and glue the two halves together, starting from the fold towards the ends, again making sure each LED sits perfectly behind another LED.

Form a ring so that the data flow starts outwards and moves inwards at the fold, follow the diagram I made.

Step 5: Solder the Ring

Remove the wires and connectors that came with the strip, if any.

Solder* the +5V/GND on the beginning of the strip to the ones on the fold, this will serve 2 functions:

  1. Structural integrity.
  2. Power injection to the middle of the strip.

*Make sure you cover the Data with tape to prevent interference.

Prepare 3 wires at your desired length and solder them to +5V, GND, and Data.

Cover the wires with heat shrink tubing.

Step 6: Assemble

Snap-fit the strip to the base, do it carefully and slowly, making sure the wires (bottom of the ring) are in the middle of one of the sections, for better structural strength.

Also, notice the data flow direction, as in the example sketch, or you will end up with a mirrored clock.

Glue the base to the wooden cover, I used super glue and some weight, and it came out perfect.

Step 7: MCU Selection

I've used an ESP8266 NodeMCU V3 (The LoLin or the CH340G variant) for serval reasons:

  1. It has a 5Vout pin connected directly to the USB.
  2. It can control a 5V strip directly without level shifting*.
  3. It is cheap.

You can still use whatever MCU you want and design your own circuit, but you will need to take care of the power supply and the level shifting between 3.3V and 5V.

*I know that it's not the best practice, but there are many NodeMCU and WS2812B projects in the makers' community taking advantage of this, so KISS, and keep a level shifter at hand if it doesn't work 😜.

Solder/Connect the wires:

  1. +5V to Vout
  2. GND to GND
  3. Data to D1

Step 8: Upload the Code

I used Arduino IDE 2.0 for uploading the code, but PlatformIO would also do the trick, copy the code or download the file below.

I think there are many ESP8266 and ESP32 webserver instructables and tutorials out there, and there is nothing special in my code, except the calculation of the position of each led and it's mirrored one facing inwards, in addition to compensating for the ring starting from the bottom and flipping the loop inwards.

*Make sure you set utcOffsetInSeconds to your location.


#include <ESP8266WiFi.h>
#include <DNSServer.h>
#include <ESP8266WebServer.h>
#include <WiFiManager.h>

#include <NTPClient.h>
#include <WiFiUdp.h>

#include <FastLED.h>

#define LED_PIN 5
#define NUM_LEDS 120
#define BRIGHTNESS 255
#define LED_TYPE WS2812B
#define COLOR_ORDER GRB

CRGB leds[NUM_LEDS];

// Set web server port number to 80
ESP8266WebServer server(80);

long utcOffsetInSeconds = 7200;

// Define NTP Client to get time
WiFiUDP ntpUDP;
NTPClient timeClient(ntpUDP, "pool.ntp.org", utcOffsetInSeconds);

// fast simulation
bool isSimOn = false;
bool isMarksOn = true;
bool isFadeOn = false;
int period = 100;

void setup() {

  Serial.begin(115200);

  // power-up safety delay
  delay(3000);

  // FastLED init
  FastLED.addLeds<LED_TYPE, LED_PIN, COLOR_ORDER>(leds, NUM_LEDS).setCorrection(TypicalLEDStrip);
  FastLED.setBrightness(BRIGHTNESS);
  fill_solid(leds, NUM_LEDS, CHSV(255, 255, 0));

  // WiFiManager
  // Local initialization. Once its business is done, there is no need to keep it around
  WiFiManager wifiManager;

  // Uncomment and run it once, if you want to erase all the stored information
  //wifiManager.resetSettings();

  wifiManager.autoConnect("AutoConnectAP");
  Serial.println("Connected.");

  timeClient.begin();
  server.on("/", handle_main);
  server.on("/sim", handle_sim);
  server.on("/time", handle_time);
  server.on("/marks", handle_marks);
  server.on("/fade", handle_fade);
  server.begin();
  Serial.println("HTTP server started");

  timeClient.update();
  Serial.println("NTP server updated");
  // power-up safety delay
  delay(2000);
}

void loop() {
  server.handleClient();
  handleClock();
  FastLED.show();
  if (isSimOn) simulateFast();
}

void simulateFast() {
  EVERY_N_MILLISECONDS(period) {
    utcOffsetInSeconds += 60;
    timeClient.setTimeOffset(utcOffsetInSeconds);
    Serial.println(timeClient.getFormattedTime());
  }
}

void handleClock() {
  timeClient.update();

  if (isFadeOn) {
    fadeToBlackBy(leds, NUM_LEDS, 1);
  } else {
    FastLED.clear();
  }

  if (isMarksOn) {
    leds[29].setRGB(25, 25, 25);
    leds[90].setRGB(25, 25, 25);  //0
    leds[44].setRGB(25, 25, 25);
    leds[75].setRGB(25, 25, 25);  //15
    leds[59].setRGB(25, 25, 25);
    leds[60].setRGB(25, 25, 25);  //30
    leds[14].setRGB(25, 25, 25);
    leds[105].setRGB(25, 25, 25);  //45
  }  

  int secPos = timeClient.getSeconds() - 31;
  secPos = secPos < 0 ? 60 + secPos : secPos;
  int secMirPos = 60 - secPos + 59;
  leds[secPos].setRGB(255, 0, 0);
  leds[secMirPos].setRGB(255, 0, 0);

  int minPos = timeClient.getMinutes() - 31;
  minPos = minPos < 0 ? 60 + minPos : minPos;
  leds[minPos].setRGB(0, 255, 0);

  int hrPos = timeClient.getHours();
  hrPos = hrPos > 11 ? hrPos - 12 : hrPos;
  hrPos = (hrPos * 5) + (timeClient.getMinutes() / 12) - 31;
  hrPos = hrPos < 0 ? 60 + hrPos : hrPos;
  hrPos = 60 - hrPos + 59;

  leds[hrPos].setRGB(0, 0, 255);
}

void handle_sim() {
  isSimOn = !isSimOn;
  if (!isSimOn) timeClient.setTimeOffset(10800);
  server.send(200, "text/plain", isSimOn ? "Sim on" : "Sim off");
}

void handle_marks() {
  isMarksOn = !isMarksOn;
  server.send(200, "text/plain", isMarksOn ? "Marks on" : "Marks off");
}

void handle_fade() {
  isFadeOn = !isFadeOn;
  server.send(200, "text/plain", isFadeOn ? "Fade on" : "Fade off");
}

void handle_main() {
  server.send(200, "text/plain", "main");
}

void handle_time() {
  timeClient.update();

  String timeStr = (String)timeClient.getHours() + ":" + (String)timeClient.getMinutes() + ":" + (String)timeClient.getSeconds();

  server.send(200, "text/plain", timeStr);
}

Step 9: Hang It, Control It and Enjoy

A small nail will do the job, you can align it just below the 12 hours mark LED, and then disable the white hours' marks.

The first time you turn it on WiFiManager will kick in, the device will set up an AP, connect your phone WiFi to it, and set the preferred hotspot for the clock to sign into.

Now you can head to the clock's web server IP address and control the clock:

  1. http://[IP ADDRESS] - connectivity check.
  2. http://[IP ADDRESS]/time - force update from NTP and return the current time.
  3. http://[IP ADDRESS]/marks - toggle the white hours' marks on/off.
  4. http://[IP ADDRESS]/fade - toggle fade mode on/off.
  5. http://[IP ADDRESS]/sim - toggle fast-forwards simulation on/off (for testing).

Step 10: What's Next?

  1. Improve the web UI/UX, and make it more user-friendly.
  2. Allow online color changes.
  3. OTA update support.
  4. Design a casing for the MCU.
  5. Design a dedicated PCB with a proper power source, maybe using my previous instructable.
  6. Adding animations, user-controlled or every X time.
  7. Settings persistence between restarts.
  8. Any ideas? I would love to hear.
Clocks Contest

Participated in the
Clocks Contest