Two-Four Timepiece – 24 hour clock

Introducing the latest clock from WoodUino – The Two-Four Timepiece 24 hour analogue clock. The Two-Four Timepiece shows a complete day at a time, and marks out time with an hour hand, minute circles and a gently pulsing seconds marker. Sunrise and sunset times are displayed and the display’s colour scheme changes between daytime and nighttime.

TwoFourTimepiece

Two-Four Timepiece at 10:29PM showing the nighttime colour scheme

TwoFourTimepiece

Two-Four Timepiece at 8:37AM showing (not very clearly) the daylight colour scheme

The Two-Four Timepiece display is built using a total of 89 addressable LEDs arranged in strips that radiate from a central point. The strips are adhered to a perforated PCB substrate cut into a squared-base circle shape. The LED strip is controlled by a discrete Arduino UNO and a battery-backed up real-time clock that keeps accurate local time. Sunrise and sunset times are determined using the user’s location (longitude and latitude) and GMT offset (using the DST algorithms previously described) using the sundata.h library, as used in the Graphic LCD Analogue Clock and SolarTracer.

In the Arduino software, I have mapped the LEDs (0 – 88) into sets of circles (outer to inner), lines (12 o’clock noon in a clockwise fashion) and spirals (from the horizontal and radiating outwards in a clockwise manner). These mappings are used to display the animated start-up sequence (I’ll let you figure this out) and the clock face.

The clock face consists of an outer daylight / nighttime background pattern on the rim, a three LED hour hand, and two rings of minutes. The outer ring provides a background to the display and is coloured separately for daylight and night time, the colouring determined by the calculations of the times of sunrise and sunset. A separate colour is used to mark the 12 noon position (North) in an effort to orientate the viewer to the display, especially when viewed at night. The three LEDs for the hour “hand” mark out the hour, with 12 noon in the North position. Minutes are displayed using the next smaller two rings such that the outer ring counts off minutes in 5-minute increments (0 – North, 5, 10, 15, etc. in a clockwise fashion) while the inner ring adds additional minutes that illuminate to display 1, 2, 3 and 4 minutes. These are coloured separately and displayed at a lower intensity from the 5-minute ring. The central LED is used to display the seconds and is controlled to slowly pulse in intensity every 2 seconds. The following sequence shows how the two minute rings work together to display each minute.

TwoFourTimepiece

Two-Four Timepiece at 10:29PM

TwoFourTimepiece

Two-Four Timepiece at 10:30PM

TwoFourTimepiece

Two-Four Timepiece at 10:31PM

The display is built using radially arranged addressable LED strips glued to a perforated PCB substrate. Lengths of LED strips are cut from a longer strip and stuck down to the substrate. Around the periphery of the display header pins are manually bent by 90º and soldered to the power (+5V), ground and signal (Data In, and Data Out) pins, with additional pins soldered to the ends of each strip. The signal pins are wired in a “daisy-chain” to create a single continuous serial control path through all the LEDs of the display. The display took just under 1 1/2 metres of 60 LEDs /metre strips.

While obvious to some, the path of the control signal through the LEDs takes some thought and it’s worth the time to get it right. In the image showing the LEDs, you can see small printed arrows on the strips pointing in the direction of the signal path. Note that there are different designs of addressable LED strips on the market and while electrically interchangeable, the strip connections are oriented differently.

If you look closely, you will see that the first LED (LED0) is located at the most southerly point of the long vertical strip – this represents the start of the LED string and is connected to an Arduino pin. The data out (DO) (after LED10) at the northerly point of this strip is connected to the data in (DI) of the next clockwise strip, and so on. This minimizes the lengths of the interconnections of the serial path between strips and makes the whole LED mapping a little easier to follow. I used wire-wrapping to make this serial path as it is quick to make connections and easy to correct in the event of an error.

Two-Four Timepiece

Two-Four Timepiece showing arrangement of LED strips

Two-Four Timepiece

Two-Four Timepiece showing wiring of power and ground rings and serial control signal path

Here’s the Arduino code for the initial build of the Two-Four Timepiece 24 hour analogue clock.

[codesyntax lang=”php” title=”Two-Four Timepiece – Build 1, Version 1 Arduino Code” blockstate=”collapsed”]

//**************************************************************************************************
//                           Two-Four Timepiece ANALOGUE 24 HOUR CLOCK
//                                   Adrian Jones, May 2015
//
//**************************************************************************************************

// Build 1
//   r1 150528 - initial build with addressable LEDs and RTC

//********************************************************************************************
#define build 1
#define revision 1
//********************************************************************************************

#include <avr/pgmspace.h>   // used for storage in program memory

// LED strip
#include "FastLED.h"        // addressable LED strip driver
#define DATA_PIN 5          // Data transfer pin
#define LED_COUNT 105       // total number of LEDs in display   
CRGB leds[LED_COUNT];       // establish array

// hue and intensity settings
#define shue   212          // seconds hue
#define mhue    95          // minutes hue (5, 10, 15...)
#define ihue   116          // intermediate mins (%5) hue
#define hdhue   53          // daytime hour hue
#define hnhue  170          // nighttime hour hue 
#define bhhue  212          // background highlight hue

#define sval   100          // seconds max intensity
#define mval   100          // minutes intensity
#define ival    50          // intermediate mins intensity
#define mnval   30          // minutes nighttime intensity
#define hdval  100          // hours daytime intensity
#define hnval   50          // hours nighttime intensity
#define bval    20          // background intensity
#define bhval   50          // background highlight intensity
int sv=0;                   // seconds intensity
boolean brighten=true;

// RTC
#include <Wire.h>           // comms protocol
#include "RTClib.h"         // RTC library
RTC_DS1307 RTC;             // instance of RTC
int hr,hr24,mn,se,dy,mo,yr,dw;  // time variables
int osec=-1,omin=-1,ohr24=-1;   // old time variables

// Sun Position library
#include <sundata.h>        // sun position library
#define LONGITUDE -75.64316 // current location longitude
#define LATITUDE  45.66167  // current position latitude
sundata vdm(0,0,0);         // create instance... see later

// cardinal directions
const char ca0[]  PROGMEM = "N  ";
const char ca1[]  PROGMEM = "NNE";
const char ca2[]  PROGMEM = "NE ";
const char ca3[]  PROGMEM = "ENE";
const char ca4[]  PROGMEM = "E  ";
const char ca5[]  PROGMEM = "ESE";
const char ca6[]  PROGMEM = "SE ";
const char ca7[]  PROGMEM = "SSE";
const char ca8[]  PROGMEM = "S  ";
const char ca9[]  PROGMEM = "SSW";
const char caA[]  PROGMEM = "SW ";
const char caB[]  PROGMEM = "WSW";
const char caC[]  PROGMEM = "W  ";
const char caD[]  PROGMEM = "WNW";
const char caE[]  PROGMEM = "NW  ";
const char caF[]  PROGMEM = "NNW";
const char * const cas[] PROGMEM = { ca0,ca1,ca2,ca3,ca4,ca5,ca6,ca7,ca8,ca9,caA,caB,caC,caD,caE,caF };

char cb[5];                  // character buffer for character strings (set to length of longest string)
float sunup, sundn;          // sunrise and sunset times (as decimal) 
boolean dtim;                // daytime? (true/false)
static int TZ;               // time zone GMT offset (-4, -5 depending on DST)
boolean bgnd = false;        // writen background? (true/false)

#define SERIAL_BAUDRATE 57600

// LED Display Mapping
#define d 0xFF                    // delimeter for arrays

const char circles[][24] PROGMEM = {                                            // all circles from outer, starting at 12 o'clock
    {10,11,17,18,24,25,28,35,36,42,43,49, 0,50,56,57,63,64,71,72,78,79,85,86},  // first (outer)
    { 9,12,16,19,23,26,29,34,37,41,44,48, 1,51,55,58,62,65,70,73,77,80,84,87},  // second
    { 8,13,15,20,22,27,30,33,38,40,45,47, 2,52,54,59,61,66,69,74,76,81,83,88},  // third
    { 7,14,21,31,39,46, 3,53,60,68,75,82, d},                                   // forth
    { 6,32, 4,67, d},                                                           // fifth
    { 5, d}                                                                     // centre
};

const char lines[][6] PROGMEM = {          // all lines starting from 12 o'clock...
    {10,9,8,7,6,5},         // line 0  12 noon (North)
    {11,12,13,5,d},         // line 1
    {17,16,15,14,5,d},      // line 2
    {18,19,20,5,d},         // line 3
    {24,23,22,21,5,d},      // line 4
    {25,26,27,5,d},         // line 5
    {28,29,30,31,32,5},     // line 6  6PM (East)
    {35,34,33,5,d},         // line 7
    {36,37,38,39,5,d},      // line 8
    {42,41,40,5,d},         // line 9
    {43,44,45,46,5,d},      // line 10
    {49,48,47,5,d},         // line 11
    {0,1,2,3,4,5},          // line 12  12 midnight (South)
    {50,51,52,5,d},         // line 13 
    {56,55,54,53,5,d},      // line 14
    {57,58,59,5,d},         // line 15
    {63,62,61,60,5,d},      // line 16
    {64,65,66,5,d},         // line 17
    {71,70,69,68,67,5},     // line 18  6AM (East)
    {72,73,74,5,5,d},       // line 19 
    {78,77,76,75,5,d},      // line 20
    {79,80,81,5,d},         // line 21
    {85,84,83,82,5,d},      // line 22
    {86,87,88,5,d}          // line 23

};

const char spirals[][21] PROGMEM = {
  { 5, 4,67, 6,32,53,75,14,39,59,81,20,40,62,84,23,44,64,86,25,49},  // spiral 1
  { 5,67, 6,32, 4,75,14,39,53,83,22,45,61,87,26,48,65,10,28, 0,71},  // spiral 2
  {82,21,46,60, 8,30, 2,69,12,34,51,73,17,36,56,78, d},              // spiral 3
  { 5,68, 7,31, 3,74,13,33,52,77,16,37,55,79,18,42,57, d},           // spiral 4
  {68, 7,31, 3,76,15,38,54,80,19,41,58,85,24,43,63, d},              // spiral 5
  { 5,60,82,21,46,66,88,27,47,70, 9,29, 1,72,11,35,50, d},           // spiral 6
  {68, 7,31, 3,76,15,38,54,80,19,41,58,85,24,43,63, d}               // spiral 7
};

const char nums[][16] PROGMEM = {
  {84,87,9,12,15,7,82,75,68,67,5,32,31,d},                           // "2"
  {4,3,2,1,0,53,54,55,51,48,44,d}                                    // "4"
};

// eeprom storage
#include <EEPROM.h>               // for alarm and mode data storage                     
int dst;                          // dst setting (true/false)                      
int *set[] = { &dst };            // values to set/recover from EEPROM

// second hand timer
long interval = 20;               // time between changing intensity
long previousMillis = 0;          // old count
unsigned long currentMillis;      // current counter


//*****************************************************************************************//
//                                      Initial Setup
//*****************************************************************************************//
void setup()  {
  
  Serial.begin(SERIAL_BAUDRATE);
  Serial.println(F("ANALOGUE 24 HOUR CLOCK by Adrian Jones"));
  Serial.print(F("Build ")); Serial.print(build); Serial.print(F(".")); Serial.println(revision);
  Serial.print(F("Free RAM: "));  Serial.print(freeRam()); Serial.println(F("B"));
  Serial.println(F("**************************************"));

  FastLED.addLeds<WS2812B, DATA_PIN, GRB>(leds, LED_COUNT);   // LED Strip
  
  Wire.begin();
  RTC.begin();                                       // start RTC
  getTime();                                         // get current time

  if (! RTC.isrunning() || (String(__DATE__).lastIndexOf(String(yr)) == -1)) {
    RTC.adjust(DateTime(__DATE__, __TIME__));        // adjust if year is off or not running
    Serial.print(F("RTC reset to ")); Serial.print(__DATE__); Serial.print(F(" ")); Serial.println(__TIME__);
  } 
  // RTC.adjust(DateTime(__DATE__, __TIME__));       // reset to system time
  // RTC.adjust(DateTime(2015, 1, 18, 1, 59, 50 ));  // test time

    restoreSettings();                               // restore all EEPROM settings
  
    TZ = (IsDST)?-4:-5;                              // adjust GMT offset based on DST  
    vdm = sundata(LATITUDE, LONGITUDE, TZ);          // create sundata instance for current location

    doAllLines(hdhue,hdval,9,20,0);                  // colour all radial lines
    delay(1000);                                     // pause and ...
    doSpiral(hdhue,hdval,9,70,2);                    // do spirals
    delay(1000);                                     // pause and ...
    doAllClear();                                    // ... clear display
    doNum(0,hdhue,3,hdval);                          // "2"
    doNum(1,hnhue,3,hdval);                          // "4"
    delay(3000);                                     // pause and ...
    doAllClear();                                    // ... clear display
}


//*****************************************************************************************//
//                                      MAIN LOOP
//*****************************************************************************************//

void loop() {
  getTime();                                         // get current time
  doDSTAdjust();                                     // adjust to DST if necessary
  doSecTimer();                                      // pulse seconds timer
  
  if(se != osec) {                                   // go round once a second (no need for faster) 
    vdm.time(yr, mo, dy, hr, mn, se);                // update with current year, month, day, hour, minutes and seconds
    vdm.calculations();                              // update calculations using current time
    sunup  = vdm.sunrise_time();                     // store sunrise time in decimal form
    sundn  = vdm.sunset_time();                      // store sunset time in decimal form
    dtim = isdtim(sunup,sundn);                      // is the sun in the sky ?  
    doDayNight();                                    // do background colouring
    
    doSerialTime();                                  // write out serial data  
    
    if(mn == 0 && se == 0) doCircle(3,0,0); else doCircleDots(3, mn/5, mhue, (dtim?mval:mnval));         // 5-min circle
    if((mn%5) == 0) doCircle(4,0,0); else doCircleDots(4, (mn-1)%5, ihue, (dtim?ival:mnval));            // 1-min circle
     
    hr24 = (hr+12)%24;                               // adjust for 0-23 hours  
    if(hr24 != ohr24) {                              // if change of hours...
      doLinePart( ohr24, 0, 0, 1, 3);                // erase old hour line
      bgnd = false;                                  // restore background on outer circle
    }
    doLinePart( hr24, (dtim?hdhue:hnhue), (dtim?hdval:hnval), 0, 3);      // new hour line

    osec = se;                                       // update old times to currcnt times
    omin = mn;
    ohr24 = hr24;
  }
}


// ********************************************************************************** //
//                                    SUBROUTINES
// ********************************************************************************** //

void doSecTimer() {
  currentMillis = millis();                                      // get current timer
  if((currentMillis - previousMillis > interval)) {              // if duration expired
    sv = (brighten)? min(sval,(sv+2)): max(0,(sv-2));
    doCircle(5, shue, sv);
    if(sv >= sval || sv <= 0) brighten = !brighten;
    previousMillis = currentMillis;
  }
}

// doSerialTime prints time and date and other information
void doSerialTime() {
  Serial.print(F("DateTime:\t"));
  Serial.print(yr); Serial.print(F("/")); if(mo<10) Serial.print(F("0")); Serial.print(mo); Serial.print(F("/")); if(dy<10) Serial.print(F("0")); Serial.print(dy); Serial.print(F(" "));
  if(hr<10) Serial.print(F("0")); Serial.print(hr); Serial.print(F(":")); if(mn<10) Serial.print(F("0")); Serial.print(mn); Serial.print(F(":")); if(se<10) Serial.print(F("0")); Serial.print(se); 
  Serial.print(F("\tSunrise: ")); Serial.print(int(sunup)); Serial.print(F(":")); if(int((sunup - int(sunup))*60.0) <10) Serial.print(F("0")); Serial.print(int((sunup - int(sunup))*60.0));
  Serial.print(F(" Sunset: "));  Serial.print(int(sundn)); Serial.print(F(":")); if(int((sundn - int(sundn))*60.0) <10) Serial.print(F("0")); Serial.print(int((sundn - int(sundn))*60.0));
  Serial.print(F(" Daytime? ")); Serial.print(dtim?"yes":"no");
  Serial.println(); 
}


// ********************************************************************************** //
//                                    DISPLAY ROUTINES
// ********************************************************************************** //

void doNum(byte num, byte col, byte cinc, byte val) {
  for (byte j=0; j < sizeof(nums[0]); j++) {
    if(pgm_read_byte( &nums[num][j] ) == d) break;
    leds[pgm_read_byte( &nums[num][j]) ] =  CHSV( col, 255, val );
    col = (col+cinc)%256;
   }
   FastLED.show(); 
} 

void doDayNight() {
  if(bgnd) return;
  for(byte x=0; x<24; x++) {
    if((x+12)%24 >= int(sunup+0.5) && (x+12)%24 < int(sundn+0.5)) doCircleDot(0, x, hdhue, bval); else doCircleDot(0, x, hnhue, bval); 
  }
  doCircleDot(0, 0, bhhue, bhval);
  bgnd = true;
}

void doAllClear() {
  for (byte j=0; j<LED_COUNT;j++) leds[j] =CHSV(0,0,0);
}

void doSpirals(byte col, byte val, byte col_inc, byte step_delay, byte repeats) {
  for (byte x = 0; x < repeats; x++) {
    for (int i = 0; i < (sizeof(spirals)/sizeof(spirals[0])); i++) {
      for (byte j=0; j < (sizeof(spirals[i])/sizeof(spirals[0][0])); j++) {
        if(pgm_read_byte( &spirals[i][j] ) == d) break;
        leds[pgm_read_byte( &spirals[i][j] )] =  CHSV( col, 255, val );
      }
      col = (col+col_inc)%256;
      FastLED.show(); 
      delay(step_delay);
    }
  }
}

void doSpiral(byte col, byte val, byte col_inc, byte step_delay, byte repeats) {
  for (byte x = 0; x < repeats; x++) {
    for (int i = 0; i < (sizeof(spirals)/sizeof(spirals[0])); i++) {
      doAllClear();
      for (byte j=0; j < (sizeof(spirals[i])/sizeof(spirals[0][0])); j++) {
        if(pgm_read_byte( &spirals[i][j] ) == d) break;
        leds[pgm_read_byte( &spirals[i][j] )] =  CHSV( col, 255, val );
        col = (col+col_inc)%256;
      }
      FastLED.show(); 
      delay(step_delay);
    }
  }
}

void doAllLines(byte col, byte val, byte col_inc, byte step_delay, byte from) {
  for (int i = 0; i < (sizeof(lines)/sizeof(lines[0])); i++) {
    for (byte j=from; j < (sizeof(lines[i])/sizeof(lines[0][0])); j++) {
      if(pgm_read_byte( &lines[i][j] ) == d) break;
      leds[pgm_read_byte( &lines[i][j] )] =  CHSV( col, 255, val );
    }
    FastLED.show(); 
    delay(step_delay);
    col += col_inc;
  }
}

void doLine(byte line, byte col, byte val, byte num) {
  for (byte j=0; j < (sizeof(lines[line])/sizeof(lines[line][0])); j++) {
    if(pgm_read_byte( &lines[line][j] ) == d ||  (j >= num)) break;
    leds[pgm_read_byte( &lines[line][j] )] =  CHSV( col, 255, val );
  }
  FastLED.show(); 
}

void doLinePart(byte line, byte col, byte val, byte from, byte to) {
  for (byte j=from; j < to; j++) {
    if(pgm_read_byte( &lines[line][j] ) == d) break;
    leds[pgm_read_byte( &lines[line][j] )] =  CHSV( col, 255, val );
  }
  FastLED.show(); 
}

void doCircle(byte num, byte col, byte val) {
  for (byte j=0; j < (sizeof(circles[num])/sizeof(circles[num][0])); j++) {
    if(pgm_read_byte( &circles[num][j] ) == d) break;
    leds[pgm_read_byte( &circles[num][j] )] =  CHSV( col, 255, val );
  }
  FastLED.show(); 
 }
 
 void doCircleDots(byte circle, byte num, byte col, byte val) {
   if(num == 0) {
     doCircleDot(circle, 0, col, val);
     return;
   }
   for(byte x=0; x<num+1; x++) {
     doCircleDot(circle, x, col, val);
   }
 }

void doCircleDot(byte circle, byte num, byte col, byte val) {
    leds[pgm_read_byte( &circles[circle][num] )] =  CHSV( col, 255, val );
 }
  

/* ********************************************************************************** //
//                                       TIME ROUTINES
// ********************************************************************************** //
   In most of Canada dtim Saving Time begins at 2:00 a.m. local time on the second Sunday in March. 
   On the first Sunday in November areas on DST return to Standard Time at 2:00 a.m. local time. 
*/

// IsDST returns true if DST, false otherwise
boolean IsDST() {
  if (mo < 3 || mo > 11) { return false; }                // January, February, and December are out.
  if (mo > 3 && mo < 11) { return true;  }                // April to October are in
  int previousSunday = dy - dw;                               
  if (mo == 3) { return previousSunday >= 8; }            // In March, we are DST if our previous Sunday was on or after the 8th.
  return previousSunday <= 0;                             // In November we must be before the first Sunday to be DST. That means the previous Sunday must be before the 1st.
}

// getTime obtains current time from RTC
void getTime() {
  DateTime now = RTC.now();
  hr = now.hour(); if(hr==0) hr=24;                      // adjust to 1-24
  mn = now.minute();
  se = now.second();
  yr = now.year();
  mo = now.month();
  dy = now.day();
  dw = now.dayOfWeek();
}

// getCardinal: converts the headsing (in degrees) to a text-based cardinal heading ("ENE", for example)
String getCardinal( float h ) { 
  h += 11.25; if (h > 360.0) h = h-360.0;
  strcpy_P(cb, (char*) pgm_read_word(&(cas[(int) (h / 22.5)]) ) );
  return cb;  
}

// doDSTAdjust increments (or decrements) by one hour when entering (or leaving) DST
void doDSTAdjust() {
  if(dst == IsDST()) return;                    // if prior setting is same as DST setting, do nothing
  if(hr != 2) return;                           // do nothing until 2pm

  DateTime now = RTC.now();                     // get time
  if(IsDST() && !dst) {
    DateTime newTime (now.unixtime() + 3600);   // add one hour
    RTC.adjust(newTime);                        
  }
  if(!IsDST() && dst) {
    DateTime newTime (now.unixtime() - 3600);   // subtract one hour
    RTC.adjust(newTime);
  }
  dst = IsDST(); 
  saveSettings();                // save change to DST
}

boolean isdtim(float up, float down) {
  float ftime = hr+(mn/60.0);
  return (ftime >= up && ftime < down);
}


// ********************************************************************************** //
//                                       EEPROM ROUTINES
// ********************************************************************************** //
// EEPROM save, restore and set/save defaults
void saveSettings()    { for(int addr = 0; addr < sizeof(set)/2; addr++) { EEPROM.write(addr, *set[addr]); } }    // save
void restoreSettings() { for(int addr = 0; addr < sizeof(set)/2; addr++) { *set[addr] = EEPROM.read(addr); } }   // recover EEPROM value


// ********************************************************************************** //
//                                      OPERATION ROUTINES
// ********************************************************************************** //
// FREERAM: Returns the number of bytes currently free in RAM  
int freeRam(void) {
  extern int  __bss_end, *__brkval; 
  int free_memory; 
  if((int)__brkval == 0) {
    free_memory = ((int)&free_memory) - ((int)&__bss_end); 
  } 
  else {
    free_memory = ((int)&free_memory) - ((int)__brkval); 
  }
  return free_memory; 
}

[/codesyntax]

See, it’s not just all about the beer!

Come on... leave a comment...