There are no shortage of DIY clocks on the internet, but inspiration from this Hack a Day article made us think there is room for at least one more. One major different with ours is we didn't want to wait 30+ years to finish the project so we took the easy way out with a 93 x WS2812B LED setup in a 6 ring configuration. We had a HelTec WiFi 32 Dev Board 'in stock' with a bad display so that became the microcontroller of choice.
-----
The schematic is super simple. In addition just showing the boring time we added a "Disco Mode" button that creates a light show (video below).
We needed a box also. Here are the 3D Printer files: Box and Cover.
Bolt it all together and this is what is does: Connects to WiFi, gets current time of day, displays the time. A quick press of the BRIGHTNESS/RESET button will cycle through 10 LED brightness levels and if you press and hold the rig will RESET. The DISCO MODE button will put on a LED light show for 10 secs. The light show also displays for 60 seconds at the top of the hour.
-----
Of course, the software is what makes it work. The Arduino IDE software is below (yes, ChatGPT helped make things both easier and harder depending on the task):/*
* "LED Ring Clock" shows time using 6 Ring WS2812B 93-LED Strip
* Outer ring=0-31, 5th=32-55, 4th=56-71, 3rd=72-83, 2nd=84-91, Center=92
*
* HelTec Automation(TM) ESP32 Series Dev boards OLED (OLED not used. Selected this uC only because we had it.)
* Adruino IDE Board Setting: WiFi Kit32, Disabled, 240MHz, 921600, None
*
* DEC 2024
* Project details at: whiskeytangohotel.com
*
*/
#include <Adafruit_NeoPixel.h>
#include <WiFi.h>
#include <time.h>
// Configuration for LED strip
#define LED_PIN 5 // Pin where the LED strip is connected
#define NUM_LEDS 93 // Total number of LEDs in the strip
#define BUTTON_PIN 4 // Pin connected to NO switch button to control Brightness and 'long press' RESET
#define DISCO_BUTTON_PIN 15 // Pin connected to button for Disco Mode
// Define up some variables
// Wi-Fi credentials
const char* ssid = "URSSID"; // Replace with your Wi-Fi SSID
const char* password = "URPASSWORD"; // Replace with your Wi-Fi password
int i = 0;
unsigned long ringTime = 1000; // later adjust ringTime to outer ring 1 sec round trip time
unsigned long endTime = 0;
unsigned long lastTimeUpdate = 999999999; // set in printLocalTime() Function to control how often we check the time server
unsigned long discoStartTime = 0; // Tracks when Disco Mode started
unsigned long buttonPressStart = 0; // Timestamp when the button was first pressed
bool buttonPressed = false; // Tracks the current NO Switch button state on PIN 4
int brightnessLevel = 3; // 0 - 255, but we cycle 1-10 cuz is bright. 100 is really really damn bright!
int currentHour = 0;
int currentMinute = 0;
int currentSecond = 0;
int LEDseconds = 32;
// Timezone settings for Austin, Texas. Adjust for your timezone
const char* ntpServer = "pool.ntp.org"; // NTP server
const long gmtOffset_sec = -21600; // Offset for CST (UTC -6)
const int daylightOffset_sec = 3600; // Daylight Saving Time offset
Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUM_LEDS, LED_PIN, NEO_GRB + NEO_KHZ800);
// Set up some colors to make it easier to customize the clock
uint32_t Red = 0xFF0000;
uint32_t Green = 0x00FF00;
uint32_t Blue = 0x0000FF;
uint32_t Yellow = 0xFFFF00;
uint32_t Cyan= 0x00FFFF;
uint32_t Magenta = 0xFF00FF;
uint32_t White = 0xFFFFFF;
uint32_t Black_Off = 0x000000;
// Now, Customize by choosing the color for the selected LED function
int Middle_blink = Red; //color for the 2 inner rings
int Fast_seconds = Middle_blink; // color for the fast paced outer ring
int Normal_seconds = Middle_blink; // adjacent to the outer ring
int Minute_hand = White; // 4th from center
int Hour_hand = Minute_hand; // 3rd from center
int How_bright = brightnessLevel; // Cycles from 1 to 10
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP); // Set button pin with internal pull-up resistor
pinMode(DISCO_BUTTON_PIN, INPUT_PULLUP); // Button for Disco Mode
strip.setBrightness(brightnessLevel); // Initialize brightness
// Initialize LED strip
strip.begin();
strip.show();
strip.clear();
strip.setBrightness(How_bright);
// Connect to Wi-Fi
Serial.print("Connecting to Wi-Fi [flash RED]...");
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
Serial.print(".");
// Toggle LEDs 84 to 92 to show trying to connect
for (int j = 84; j <= 92; j++) {
uint32_t currentColor = strip.getPixelColor(j); // Get current color of the LED
if (currentColor == 0) { // If the LED is off
strip.setPixelColor(j, strip.Color(255, 0, 0));
}else { // If the LED is on
strip.setPixelColor(j, 0); // Turn it off
}
} // End j loop for center LED toggle
strip.show(); // Update the strip
delay(500);
}
Serial.println("\nWi-Fi connected [flash GREEN]!!!");
for (int i = 0; i <11; i++) { // fast flash the inner LEDs to show 'wifi connected'
for (int j = 84; j <= 92; j++) {
uint32_t currentColor = strip.getPixelColor(j); // Get current color of the LED
if (currentColor == 0) { // If the LED is off
strip.setPixelColor(j, strip.Color(0, 255, 0)); // Define RGB color
}else { // If the LED is on
strip.setPixelColor(j, 0); // Turn it off
}
} // End j loop for center LED toggle
strip.show(); // Update the strip
delay(200);
}
// Center LED Red to show we are trying to get the time
strip.setPixelColor(92, strip.Color(255, 0, 0)); // Define RGB color
strip.show(); // Update the strip
delay(100);
// Initialize time
configTime(gmtOffset_sec, daylightOffset_sec, ntpServer);
printLocalTime(); // Print the current local time to the Serial Monitor
}
void loop() {
// Scanner effect for the first 32 LEDs on the outter ring
unsigned long startTime = millis(); // We time the loop and aim for 1 sec lap time
for (i = 0; i < 32; i++) {
handleButtonPress(); // is the Button pressed for Brightness or RESET?
runDiscoMode(); // Disco Mode pressed?
strip.setPixelColor(i, Fast_seconds); // Define RGB color
strip.show(); // Update the strip
unsigned long elapsedTime = millis() - startTime; // Calculate elapsed time
unsigned long delayPerLED = (1000 - elapsedTime) / (32 - i); // Dynamically adjust delay
delay(delayPerLED); // Apply calculated delay
strip.setPixelColor(i, strip.Color(0, 0, 0)); // Turn off the current LED
if (i == 0) { // Update the clock 'hands'
// Toggle LEDs 84 to 92 (the center LEDs at 1Hz)
for (int j = 84; j <= 92; j++) {
uint32_t currentColor = strip.getPixelColor(j); // Get current color of the LED
if (currentColor == 0) { // If the LED is off
strip.setPixelColor(j, Middle_blink); // ON and Define RGB color
}else { // If the LED is on
strip.setPixelColor(j, 0); // Turn it off
}
} // End j loop for center LED 1 Hz toggle
uint32_t currentColor = strip.getPixelColor(91); // Flash center LED at opposite ON/OFF of surrounding ring
if (currentColor == 0) { // If the near LED is off
strip.clear(); // This helps avoid 'artifacts' just in case
strip.setPixelColor(92, Middle_blink);
}
// Position minute hand (4th ring with 16 LEDs for limited resolution)
// 56(top) and 71 is last, so...
int minuteLED = 56 + (currentMinute / 3.75);
strip.setPixelColor(minuteLED, Minute_hand);
// Position hour hand (3rd ring with 12 LEDs. LED72=12 Noon. LED83=11]
if (int(currentHour == 12)) { // We want LED 72, not LED 12 + 72
currentHour = 0;
}
int LEDhour = int(currentHour) + 72;
strip.setPixelColor(LEDhour, Hour_hand); // Define RGB color
// End position Hour Hand
// Position the second hand (4th ring: LED 32 = TOP, LED 55 = LAST, 24 LEDs in total)
strip.setPixelColor(LEDseconds, 0); // Turn off the previous second's LED
// Update LEDseconds and ensure it stays within the range [32, 55]
LEDseconds = (LEDseconds < 55) ? LEDseconds + 1 : 32; // less than 55 then add +1 else reset LEDseconds to 32
strip.setPixelColor(LEDseconds, Normal_seconds); // Turn on the current second's LED
// End Position Second Hand
} // End if i == 0 to Update the clock 'hands'
} // End For i loop for outer 'scanner' ring
unsigned long endTime = millis();
ringTime = endTime - startTime; // How long did a full outer scanner ring take
Serial.println();
//Serial.println("Outer ring time: " + String(ringTime) + " ms");
printLocalTime(); // Print Local time
}
void printLocalTime() {
// Update from the time server every 60 secs (1 min = 60000 milliseconds)
if (millis() - lastTimeUpdate < 60000) {
// Print the current local time from global variables
Serial.print("Current local time: ");
Serial.print(currentHour);
Serial.print(":");
if (currentMinute < 10) Serial.print("0"); // Add leading zero to minutes if needed
Serial.print(currentMinute);
Serial.print(":");
if (currentSecond < 10) Serial.print("0"); // Add leading zero to seconds if needed
Serial.println(currentSecond);
Serial.println( "Next time update in: " + String(60 - (millis() - lastTimeUpdate)/1000) + " seconds." );
Serial.println("Brightness set to: " + String(brightnessLevel));
return; // Exit the function without updating the time
}
// Fetch time from the NTP server
struct tm timeinfo;
Serial.println("Fetching time from the NTP server...");
if (!getLocalTime(&timeinfo)) {
Serial.println("Failed to obtain time");
strip.setPixelColor(92, Red); // Error code on center LED
strip.show();
delay(500);
return;
}
int hour = timeinfo.tm_hour;
String meridian = "AM";
// Convert 24-hour to 12-hour format
if (hour >= 12) {
meridian = "PM";
if (hour > 12) hour -= 12; // Convert to 12-hour format
} else if (hour == 0) {
hour = 12; // Midnight in 12-hour format
}
// Update global variables
currentHour = hour;
currentMinute = timeinfo.tm_min;
currentSecond = timeinfo.tm_sec;
// Print time in 12-hour format
Serial.print("Current local time: ");
Serial.print(hour);
Serial.print(":");
if (timeinfo.tm_min < 10) Serial.print("0"); // Add leading zero to minutes if needed
Serial.print(timeinfo.tm_min);
Serial.print(":");
if (timeinfo.tm_sec < 10) Serial.print("0"); // Add leading zero to seconds if needed
Serial.print(timeinfo.tm_sec);
Serial.print(" ");
Serial.println(meridian);
// Update the last time update to the current time
lastTimeUpdate = millis();
}
void handleButtonPress() {
static bool showingBrightness = false; // Tracks if brightness feedback is being displayed
static unsigned long brightnessDisplayStart = 0; // Tracks when feedback started
bool buttonState = digitalRead(BUTTON_PIN); // Read button state
if (buttonState == LOW) { // Button pressed (LOW because of pull-up)
if (!buttonPressed) {
buttonPressed = true;
buttonPressStart = millis(); // Record the press start time
}
// Check if the button is held for 5 seconds
if (millis() - buttonPressStart >= 5000) {
Serial.println("Resetting ESP32...");
ESP.restart(); // Reset the ESP32
}
} else { // Button released
if (buttonPressed) { // Only act if it was pressed before
buttonPressed = false;
if (millis() - buttonPressStart < 5000) { // Short press
// Cycle brightness level (1 to 9)
brightnessLevel = (brightnessLevel % 9) + 1;
strip.setBrightness(brightnessLevel);
strip.show(); // Apply the new brightness
Serial.println("Brightness adjusted to: " + String(brightnessLevel));
// Start brightness feedback display
showingBrightness = true;
brightnessDisplayStart = millis();
strip.clear(); // Clear previous LED states
for (int i = 84; i < 84 + brightnessLevel; i++) {
strip.setPixelColor(i, Magenta); // Brightness level feedback via middle LEDs
}
strip.show();
}
}
}
// Handle non-blocking brightness feedback display
if (showingBrightness && (millis() - brightnessDisplayStart >= 1000)) {
// Turn off LEDs after 1 second
showingBrightness = false;
for (int i = 84; i < 92; i++) {
strip.setPixelColor(i, 0); // Turn off the LEDs
}
strip.show();
}
}
void runDiscoMode() { //Flash all the LEDs in random colors
static unsigned long lastUpdate = 0; // Tracks last LED update time
static unsigned long discoStartTime = 0; // Tracks when Disco Mode started
static bool discoActive = false; // Tracks if Disco Mode is active
static bool buttonStateLast = LOW; // Tracks previous button state
// Determine the button state of the black NC button
bool buttonState;
int TopofHour = 0; // We Disco Mode flash for the whole minute
if (currentMinute == TopofHour) {
buttonState = HIGH; // Force Disco Mode at the top of the hour
} else {
buttonState = digitalRead(DISCO_BUTTON_PIN); // Read the actual button state
}
// Toggle Disco Mode on button press (HIGH) with debounce
if (buttonState == HIGH && buttonStateLast == LOW) { // Button was just pressed
discoActive = !discoActive; // Toggle discoActive
if (discoActive) {
Serial.println("Entering Disco Mode...");
discoStartTime = millis(); // Record the start time
} else {
Serial.println("Exiting Disco Mode via button press...");
strip.clear();
strip.show(); // Clear LEDs
}
}
if (currentMinute == TopofHour) { // remain in Disco Mode
buttonState = HIGH; // Force Disco Mode at the top of the hour
} else {
buttonStateLast = buttonState; // Update last button state
}
// If Disco Mode is active
if (discoActive) {
if (millis() - discoStartTime >= 10000) { // Exit Disco Mode after set delay in milliseconds
Serial.println("Exiting Disco Mode via timeout...");
discoActive = false;
strip.clear();
strip.show(); // Clear LEDs
return;
}
// Update LEDs non-blocking
if (millis() - lastUpdate > 10) { // in mSecs delay on the flashes. i think faster is better
lastUpdate = millis();
for (int i = 0; i < NUM_LEDS; i++) {
strip.setPixelColor(i, strip.Color(random(256), random(256), random(256)));
}
strip.show();
//Serial.println("Update disco...");
}
}
}
-----