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.
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.
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.
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!