Friday, December 20, 2024

WS2812B LED Ring Clock

 -----

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

-----