Idea

After building my first Neopixel Clock, I decided I needed one for myself. There was no way I was going to solder 90 lengths of wire onto 180 tiny pads again, though, so I knew I needed to design a custom PCB. This necessitated a redesign of the entire clock, focused around making it as easy as possible to assemble.

Design

Having been unsatisfied with Kicad the last time I designed a PCB, I looked for new options, and found EasyEDA. They are a PCB design web app plus manufacturing service all in one. I found the web app to work quite nicely for my needs, and their PCBs were both high quality and cheap, so I’m happy with them so far.

Custom PCB populated with Neopixel strip segments

Digit diffuser in CAD

To minimize the cost and the size of the finished product, I designed a single circuit board that could hold Neopixel strips in a 7-segment arrangement, plus all of the electronics needed to drive the clock. Each digit uses the same circuit board, with the holes for the microcontroller and clock module left unpopulated in all but the first digit. Power and data come in from the left and exit out the right.

After designing the circuit boards, I designed a new light diffuser around the board shape. The circuit board slides and snaps in from the back of the diffuser, forming a separate enclosure around each of the seven Neopixel strip segments.

Hardware

Like the previous version, the clock is powered by an Arduino Pro Mini clone. It communicates with a DS3231 Real Time Clock module via I2C. Power comes over USB, via a micro USB breakout board. The Neopixel strips are 60-per-meter WS2812B LEDs. There are two LEDs per digit segment, for a total of 14 on each digit and 58 individually-addressable lights on the entire clock.

Software

The software (full source code below) was reused from my original Neopixel clock. I updated the arrays to fit a clock with a different number and arrangement of pixels, and it worked straight away.

Result

 

Gerber Files

Addressable LED Clock

Code

#include <Adafruit_NeoPixel.h>
#ifdef __AVR__
  #include <avr/power.h>
#endif

#define PIN 9

#define SEGMENTS_PER_DIGIT 7
#define LEDS_PER_SEGMENT 2
#define LEDS_PER_DIGIT 14
#define GRID_WIDTH 17
#define GRID_HEIGHT 7

#define NUMPIXELS 58

#include <Wire.h>
#include "RTClib.h"
#include <Encoder.h>

RTC_DS3231 rtc;

Adafruit_NeoPixel strip = Adafruit_NeoPixel(NUMPIXELS, PIN, NEO_GRB + NEO_KHZ800);

int mode = 0;
bool justEnteredMode = true;

double timeOfLastInput = 0;
double timeOfLastRefresh = 0;
int inactivityTimeoutDuration = 3000;

uint32_t startColor,endColor;

uint16_t startHue = 0;
uint16_t endHue = 80;

int brightness = 10;


int buttonPin = 10;
bool oldButtonState = 1;

Encoder myEnc(3, 2);
long oldPosition = -999;
int inputHours = 1;
int inputTenMinutes = 0;
int inputMinutes = 0;

int inputColor = 0;
int inputColorShift = 12;
int inputColorDirection = 0;


// Defines the light number for each light by its position in a grid covering the whole clock face
// 999 indicates positions where no light exists

const int PROGMEM pixelNumber[7][17] = 
{
  {999,  0,  1,999,999, 14, 15,999,999,999, 30, 31,999,999, 44, 45,999},
  { 11,999,999,  2, 25,999,999, 16,999, 41,999,999, 32, 55,999,999, 46},
  { 10,999,999,  3, 24,999,999, 17, 29, 40,999,999, 33, 54,999,999, 47},
  {999, 12, 13,999,999, 26, 27,999,999,999, 42, 43,999,999, 56, 57,999},
  {  9,999,999,  4, 23,999,999, 18, 28, 39,999,999, 34, 53,999,999, 48},
  {  8,999,999,  5, 22,999,999, 19,999, 38,999,999, 35, 52,999,999, 49},
  {999,  7,  6,999,999, 21, 20,999,999,999, 37, 36,999,999, 51, 50,999},
};


const int pixelValues[][14] = {
  {1,1,1,1,1,1,1,1,1,1,1,1,0,0}, // Number 0
  {0,0,1,1,1,1,0,0,0,0,0,0,0,0}, // Number 1
  {1,1,1,1,0,0,1,1,1,1,0,0,1,1}, // Number 2
  {1,1,1,1,1,1,1,1,0,0,0,0,1,1}, // Number 3
  {0,0,1,1,1,1,0,0,0,0,1,1,1,1}, // Number 4
  {1,1,0,0,1,1,1,1,0,0,1,1,1,1}, // Number 5
  {1,1,0,0,1,1,1,1,1,1,1,1,1,1}, // Number 6
  {1,1,1,1,1,1,0,0,0,0,0,0,0,0}, // Number 7
  {1,1,1,1,1,1,1,1,1,1,1,1,1,1}, // Number 8
  {1,1,1,1,1,1,1,1,0,0,1,1,1,1}, // Number 9
  {0,0,0,0,0,0,0,0,0,0,0,0,0,0}  // Digit off
};


void setup () {

  Serial.begin(9600);


  if (! rtc.begin()) {
    Serial.println("Couldn't find RTC");
    while (1);
  }
    
  if (rtc.lostPower()) {
    Serial.println("RTC lost power, lets set the time!");
    // following line sets the RTC to the date & time this sketch was compiled
    rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
    // This line sets the RTC with an explicit date & time, for example to set
    // January 21, 2014 at 3am you would call:
    // rtc.adjust(DateTime(2014, 1, 21, 3, 0, 0));
  }

  // Uncomment below to set time to compile time.
  // THIS WILL KEEP RESETTING THE TIME EVERY BOOT UNTIL CODE WITH THIS DISABLED IS UPLOADED
  //rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));


  strip.begin(); // This initializes the NeoPixel library.


  pinMode(buttonPin, INPUT_PULLUP);      // sets the digital pin as output

}

void loop () {

  if (digitalRead(buttonPin) == 0 && oldButtonState == 1) {
    mode++;
    justEnteredMode = true;
    timeOfLastInput = millis(); // Count mode switching as an input
  }

  oldButtonState = digitalRead(buttonPin);

  int delayNum = 500;

  DateTime now = rtc.now();
  

  int convertedHour;

  if (now.hour() > 12) {
    convertedHour = now.hour() - 12;
  }
  else {
    convertedHour = now.hour();
  }


  int num0 = (convertedHour /10) % 10;
  int num1 = convertedHour % 10;
  int num2 = (now.minute() /10) % 10;
  int num3 = now.minute() % 10;


  // Time display and brightness adjust mode
  if (mode == 0) {
    // Run only on first instance of loop
    if (justEnteredMode == true) {
      Serial.println("Mode 0, display time");
    }
    justEnteredMode = false;

    adjustVariable(&brightness, 1, 1, 20);

    updateDigits(num0,num1,num2,num3);

    inputColor += 5;
  }

  // Color set mode
  else if (mode == 1) {
    // Run only on first instance of loop
    if (justEnteredMode == true) {
      Serial.println("Mode 1, set color 1");
      updateDigits(1, 10, 10, 10);
      delay(500);
    }
    justEnteredMode = false;


    adjustVariable(&inputColor, 10, 0, 360);
    
    resetModeAfterInactivity();

    updateDigits(8, 8, 8, 8);

  }
  
  // Color shift amount set mode
  else if (mode == 2) {
    // Run only on first instance of loop
    if (justEnteredMode == true) {
      Serial.println("Mode 2, set color shift amount");
      updateDigits(2, 10, 10, 10);
      delay(500);
    }
    justEnteredMode = false;

    adjustVariable(&inputColorShift, 1, 0, 30);
    
    resetModeAfterInactivity();
        
    updateDigits(8, 8, 8, 8);

  }  
  
  // Gradient direction set mode
  else if (mode == 3) {
    // Run only on first instance of loop
    if (justEnteredMode == true) {
      Serial.println("Mode 3, set gradient direction");
      updateDigits(3, 10, 10, 10);
      delay(500);
    }
    justEnteredMode = false;

    adjustVariable(&inputColorDirection, 1, 0, 2);
    
    resetModeAfterInactivity();

    updateDigits(8, 8, 8, 8);

  }

  // Hour set mode
  else if (mode == 4) {
    // Run only on first instance of loop
    if (justEnteredMode == true) {
      Serial.println("Mode 4, set hour");
    }
    justEnteredMode = false;
    
    adjustVariable(&inputHours, 1, 1, 12);

    int digitOne = (inputHours / 10) % 10;
    int digitTwo = inputHours % 10;
    
    resetModeAfterInactivity();

    updateDigits(digitOne, digitTwo, 10, 10);

    rtc.adjust(DateTime(now.year(), now.month(), now.day(), inputHours, now.minute(), now.second()));
    
  }

  //Ten minute set mode
  else if (mode == 5) {
    // Run only on first instance of loop
    if (justEnteredMode == true) {
      Serial.println("Mode 5, set ten minutes");
    }
    justEnteredMode = false;

    adjustVariable(&inputTenMinutes, 1, 0, 5);
    
    resetModeAfterInactivity();

    updateDigits(10, 10, inputTenMinutes, 10);

    rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), inputTenMinutes*10, 0));
    
  }

  //Minute set mode
  else if (mode == 6) {
    // Run only on first instance of loop
    if (justEnteredMode == true) {
      Serial.println("Mode 6, set minute");
    }
    justEnteredMode = false;
    
    adjustVariable(&inputMinutes, 1, 0, 9);
    
    resetModeAfterInactivity();

    updateDigits(10, 10, 10, inputMinutes);

    rtc.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), (inputTenMinutes*10) + inputMinutes, 0));
    
  }
  
  else {
    mode = 0;
  }
  
}


void resetModeAfterInactivity() {

  if (timeOfLastInput + inactivityTimeoutDuration < millis()) {
    Serial.print("Time of last input: ");
    Serial.print(timeOfLastInput);
    Serial.print(". Current milis: ");
    Serial.println(millis());
    mode = 0;
  }
  
}

void updateDigits(int num0, int num1, int num2, int num3) {

  if (timeOfLastRefresh + 100 < millis()) {

    timeOfLastRefresh = millis();
    
    // Set the segments
    for (int v=0; v<GRID_HEIGHT; v++) {
    
      for (int h=0; h<GRID_WIDTH; h++) {
    
        int currentPixelNumber = pgm_read_word_near(&(pixelNumber[v][h]));
    
        if (currentPixelNumber != 999) {
    
          // There is a pixel here, see if it should be lit
          int digitOffset = 0;
  
          // Track which number we want to draw to the current digit position
          int numberToDraw;
  
          // Track if we're on a divider pixel
          bool divider = false;
          
          // First digit
          if (h >= 0 && h < LEDS_PER_SEGMENT + 2) {
            digitOffset = 0;
            numberToDraw = num0;
            if (numberToDraw == 0) {
              numberToDraw = 10;
            }
          }
  
          // Second digit
          if (h >=LEDS_PER_SEGMENT + 2 && h < (LEDS_PER_SEGMENT * 2) + 4) {
            digitOffset = LEDS_PER_SEGMENT * SEGMENTS_PER_DIGIT;
            numberToDraw = num1;
          }
  
          // Divider
          if (h == (LEDS_PER_SEGMENT * 2) + 4) {
            digitOffset = (LEDS_PER_SEGMENT * SEGMENTS_PER_DIGIT) * 2;
            divider = true;
          }
  
          if (h >= (LEDS_PER_SEGMENT * 2) + 5 && h <= (LEDS_PER_SEGMENT * 3) + 6) {
            digitOffset = (LEDS_PER_SEGMENT * SEGMENTS_PER_DIGIT) * 2 + 2;
            numberToDraw = num2;
          }
  
          if (h >= (LEDS_PER_SEGMENT * 3) + 7 && h <= (LEDS_PER_SEGMENT * 4) + 8) {
            digitOffset = (LEDS_PER_SEGMENT * SEGMENTS_PER_DIGIT) * 3 + 2;
            numberToDraw = num3;
          }
          
          if (pixelValues[numberToDraw][currentPixelNumber-digitOffset] == 1 || divider == true) {   
            if (inputColorDirection == 0) {
              strip.setPixelColor(currentPixelNumber, HSV_to_RGB(inputColor + h*inputColorShift,100,brightness)); 
            }
            else if (inputColorDirection == 1) {
              strip.setPixelColor(currentPixelNumber, HSV_to_RGB(inputColor + v*inputColorShift,100,brightness)); 
            }
            else {
              strip.setPixelColor(currentPixelNumber, HSV_to_RGB(inputColor,100,brightness)); 
            }
          }
          else {
            strip.setPixelColor(currentPixelNumber, strip.Color(0,0,0));
          }
          
          
          strip.show();
    
        }
        else {
          // 999 means no pixel here, do nothing
        }
      }
  
    }
  
  }
    
}


void adjustVariable(int *variable, int increment, int minNum, int maxNum) {

  long newPosition = myEnc.read();
  
  if (newPosition != oldPosition) {
    if (oldPosition > newPosition) {
      *variable -= increment;
      
      timeOfLastInput = millis();
      
      if (*variable < minNum) {
        *variable = maxNum;
      }  
      Serial.print("Decrease to");
      Serial.println(*variable + increment);
    }
    if (oldPosition < newPosition) {
      *variable += increment;

      timeOfLastInput = millis();
      
      if (*variable > maxNum) {
        *variable = minNum;
      }
      Serial.print("Increase to ");
      Serial.println(*variable - increment);
    }      

    oldPosition = newPosition;
  }
  
}


uint32_t HSV_to_RGB(float h, float s, float v) {
  int i;
  float f,p,q,t;

  uint8_t r,g,b;

  while (h >= 360) {
    h = h - 360;
  }
  while (h < 0) {
    h = h + 360;
  }
  
  h = max(0.0, min(360.0, h));
  s = max(0.0, min(100.0, s));
  v = max(0.0, min(100.0, v));
  
  s /= 100;
  v /= 100;
  
  if(s == 0) {
    // Achromatic (grey)
    r = g = b = round(v*255);
    return;
  }

  h /= 60; // sector 0 to 5
  i = floor(h);
  f = h - i; // factorial part of h
  p = v * (1 - s);
  q = v * (1 - s * f);
  t = v * (1 - s * (1 - f));
    
  switch(i) {
    case 0:
      r = round(255*v);
      g = round(255*t);
      b = round(255*p);
      break;
    case 1:
      r = round(255*q);
      g = round(255*v);
      b = round(255*p);
      break;
    case 2:
      r = round(255*p);
      g = round(255*v);
      b = round(255*t);
      break;
    case 3:
      r = round(255*p);
      g = round(255*q);
      b = round(255*v);
      break;
    case 4:
      r = round(255*t);
      g = round(255*p);
      b = round(255*v);
      break;
    default: // case 5:
      r = round(255*v);
      g = round(255*p);
      b = round(255*q);
  }

  return strip.Color(r,g,b);  
};

 



		

Leave a Reply

Your email address will not be published. Required fields are marked *

%d bloggers like this: