Wall Clock

WALL CLOCK is an illuminated wall clock that displays hours and minutes, and optionally, seconds, in a somewhat conventional clock format.  The clock has 60 tri-colour LEDs embedded into its periphery to shine a pleasing analogue representation of the time onto the wall on which the clock is hung.

WallClock-p5Night-time illumination showing 45 minutes past nine.

Along with the normal hour, minute and second time setting features, both the individual colour (hue) and brightness of the hours, minutes and seconds can be simply adjusted allowing matching to individual preferences and any decor.

Hues for hours, minutes and seconds anywhere on the colour wheel

There are several techniques employed to create a smooth flowing light transition and avoid abrupt changes in light intensity. At each minute, the new minute LED fades into (or fades from) the display. During even hours, the minute LEDs advance clockwise around the face. At the next hour (odd) the minute LEDs extinguish with each minute. This avoid abrupt light level changes across the top of the hour. The hour LED indication similarly fades up, advancing advances every 12 minutes towards the next hour.

WallClock-p1Daytime illumination representing 40 minutes past nine.

A central rotary mode switch allows for selection of the adjustment mode. Bidirectional rotation of the switch increments or decrements the adjustment being made. A mode LED, positioned at the 12 o’clock position, provides visual feedback as to the function being adjusted. Subsequent revision of the design has removed this LED in favour of providing visual feedback through the main strip of LEDs.

WallClock-p11

Rear view of wall clock showing components and wiring.

In addition, a light sensor, mounted through the front face, monitors ambient light conditions and adjusts the intensity of the LEDs to suit. Light levels set by the user are automatically increased by up to 50% during very bright daylight and decreased by up to 50% for night-time viewing. Additional user-programmable options include enabling/disabling the showing of seconds, and enabling/disabling a circulating pattern of LEDs every second.

MapleClockFace

Maple and Bloodwood Wall Clock Face

The unit is built using an Arduino Nano, a DS3231 Real Time Clock and a strip of 60 addressable LEDs. The unit is powered by a single 5V DC power supply. RTC battery back-up and non-volatile EEPROM ensures that the time and user settings are maintained over power interruptions. Small production runs replace the Nano with a discrete Atmega328 chip.

Here is the Arduino source code for the current build (Rev. 2, v4). Enjoy!

[codesyntax lang=”php” tab_width=”4″ strict=”yes” title=”Wall Clock Source Code” blockstate=”collapsed”]

/*******************************************************************************************
/                                      LED LIGHT STRIP
/                                   Adrian Jones, Oct. 2014
/
/********************************************************************************************/

// Rev. 2 v1:  141001
//   - removed RGB LED; LED strip used to show all mode settings;
//   - add second hand, and mode controls for setting hue, intensity and resetting to 0; 
//   - add photoresistor and sensr pin ambient light and dim LEDs accordingly (w/ 4k7 to gnd)
//   - fixed bug in EEPROM storage and recovery
//
// Rev. 2 v2: 141003
//   - added a call to set defaults if no settings previously stored in EEPROM
//
// Rev. 2 v3: 141003
//   - added running LEDs every second. LEDs run from second LED with incrementing hue
//   - new mode allows control of seconds and running seconds
//
// Rev. 2 v4: 141010
//   - changed brightness intensity setting for larger to increments
//   - time (hours and mins) flashes when set 

//********************************************************************************************
#define rev 2
#define ver 4

// Pin usage: Rotary Encoder: D2,D3,D4; LED Strip: D5; RTC A4,A5; Light sensor: A3
                                                                              
#include <Wire.h>               // I2C-WIRE library
#include "RTClib.h"             // RTC-Library
RTC_DS1307 RTC;                 // Tiny RTC (DS1307) module (SDA - A4, SCL - A5)

// Rotary Encoder and Switch
#define enc_PinA   2            // encoder A (pin 2 = interrupt 0)
#define enc_PinB   4            // encoder B to pin 4
#define enc_Switch 3            // encoder switch to pin 3 (pin 3 = interrupt 1)

boolean rotating  = false;      // encoder being rotated
boolean clockwise = false;      // direction of rotation
boolean modeFlag  = false;      // mode change flag     
byte enc_A, enc_B, enc_A_prev=0;

byte mode;                      // modes:
#define mode_normal 0           // normal
#define mode_hrshue 1           // set hour hue
#define mode_hrsval 2           // set hour brightness
#define mode_hrsadj 3           // time adjust: hours
#define mode_minhue 4           // set minute hue
#define mode_minval 5           // set minute background brightness
#define mode_minadj 6           // time adjust: minutes
#define mode_sechue 7           // set second hue
#define mode_secval 8           // set second brightness
#define mode_secadj 9           // time adjust: seconds
#define mode_optadj 10          // options adjust
#define led13 13                // built in LED for 'keep alive' indication

// LED strip
#include "FastLED.h"
#define NUM_LEDS 60             // #LEDs in strip
#define DATA_PIN 5              // Data transfer pin
CRGB leds[NUM_LEDS];            // Define the array of leds

// Operational options
byte options;                     // options byte
byte options_def = 0;            // default
byte showSecsb = 0;              // show second
byte runSecsb  = 1;              // run seconds
boolean showSecs, runSecs;


// EEPROM
#include <EEPROM.h>
byte hueMin = 0, hueMax = 255, valMin = 0, valMax = 255;
byte val_inc;
byte hue_hr_def = 33;            // default hour hue
byte hue_mn_def = 167;           // default minute hue
byte hue_se_def = 220;           // default second hue
byte val_hr_def = 100;           // initial hour brightness
byte val_mn_def = 50;            // default minute brightness
byte val_se_def = 100;           // default second brightness

byte hue_hr, hue_mn, hue_se, val_mn, val_mx, val_hx, val_hmax, val_hdec, val_hr, val_se;
byte *set[] = {&hue_hr, &hue_mn, &val_mn, &val_hr, &hue_se, &val_se, &options};
byte def[]  = {hue_hr_def, hue_mn_def, val_mn_def, val_hr_def, hue_se_def, val_se_def, options_def};

boolean minChangeFlag = false;
boolean hrChangeFlag  = false;
byte lastsec=0;

long interval = 10000;            // reset mode after this duration
long previousMillis = 0;          // old count
unsigned long currentMillis;      // current counter

DateTime now;                     // current time
byte ch,cm,cs;                    // hours, mins, and seconds

#define lsPin  A3                // light sensor pin
#define lsMax  600               // light sensor max
#define lsMin  150               // light sensor min
int lm;                          // light modulation value
#define lmMax  5                 // light modulation max
#define lmMin  -5                // light modulation min
#define lmDiv 10                 // light modulation divisor



//*****************************************************************************************//
//                                      Initial Setup
//*****************************************************************************************//
void setup() { 
  FastLED.addLeds<WS2812B, DATA_PIN, GRB>(leds, NUM_LEDS);  // LED Strip
  Wire.begin();
  
  Serial.begin(57600);
  Serial.print(F("Wall Clock - Adrian Jones, October 2014 - Revision "));
  Serial.print(rev);Serial.print(".");Serial.println(ver);
  Serial.print(F("Free RAM: "));  Serial.print(freeRam()); Serial.println(F("B"));

  // Rotary encoder and switch
  pinMode(enc_PinA, INPUT_PULLUP);   digitalWrite(enc_PinA,   HIGH);     // rotary encoder pin A
  pinMode(enc_PinB, INPUT_PULLUP);   digitalWrite(enc_PinB,   HIGH);     // rotary encoder pin B
  pinMode(enc_Switch, INPUT_PULLUP); digitalWrite(enc_Switch, HIGH);     // rotary encoder switch
  
  attachInterrupt(0, rotEncoder, CHANGE);    // time setting
  attachInterrupt(1, swEncoder,  CHANGE);    // mode change (normal, hour set, min set)

  //RTC
  RTC.begin();
  if (! RTC.isrunning()) {   
    RTC.adjust(DateTime(__DATE__, __TIME__));
    Serial.println(F("RTC reset"));
  } else {
    Serial.println(F("RTC running"));
  }

  doOptions(); 
  mode = mode_normal;                      // set mode to normal run
  restoreSettings();                       // restore saved settings
}


//*****************************************************************************************//
//                                      MAIN LOOP
//*****************************************************************************************//
void loop() {
   doOptions();
   doLightSense();                                                // sense ambient light
   currentMillis = millis();                                      // get current time
   if(!modeFlag) previousMillis = currentMillis;
   if(modeFlag && (currentMillis - previousMillis > interval) ) { // if change of mode and duration expired
     previousMillis = currentMillis;                              // previous = current,     
     mode = mode_normal;                                          // reset mode and ...
     modeFlag = false;                                            // ... reset change flag
   }
   if(mode != mode_normal && rotating) {modeFlag = true; previousMillis = currentMillis; }
 
   getTime();                                   // get current time 

   if(cs != lastsec || mode != mode_normal) {   // new second or setting mode?
     spTimeMode();                              // serial output
     showLEDmins(ch,cm,cs);                     // show minute LEDs
     showLEDsecs(cs);                           // show second LED
     showLEDhrs(ch,cm,cs);                      // set hour LED
     doModeLEDs();                              // set LEDS for appropriate mode
     FastLED.show();                            // display the LEDs
     lastsec = cs;                              // update seconds counter
     if (mode == mode_optadj) doRunningSecond();
   } else {
     doRunningSecond();                         // run LED around every second
   }

   while(rotating) {
     adjTime(now, clockwise);                   // adjust time based on rotation direction (inc/dec)
     adjColour();                               // adjust hue and brightness
     rotating = false;                          // reset the interrupt flag
   }
   
   if(mode != mode_normal) delay(100);          // slow down loop when in other than normal mode
}

// ********************************************************************************** //
//                                      INTERRUPT ROUTINES
// ********************************************************************************** //

// function rotEncoder(): called when encoder rotated
void rotEncoder(){
  delay(2);
  enc_A = digitalRead(enc_PinA); 
  enc_B = digitalRead(enc_PinB);
  if(!enc_A && enc_A_prev){                  // if change of state...
    clockwise = (!enc_A && enc_B)? false : true;   // record state
    if(mode != mode_normal) rotating = true;           // set flag
    modeFlag = true;
  } 
  enc_A_prev = enc_A;                        // Store state for next time    
}

// function swEncoder(): called when encoder button pushed
void swEncoder(){
  if(digitalRead (enc_Switch) != LOW) return;  // if switch depressed
  delay(1);                                   // debounce  
  if(digitalRead (enc_Switch) != LOW) return;  // if switch still depressed
  delay(1);                                   // debounce  
  if(digitalRead (enc_Switch) != LOW) return;  // if switch still depressed
  mode = (++mode) % 11;            // increment mode
  modeFlag = true;                             // set change of mode flag
  previousMillis = currentMillis;
}

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


// function spTimeMode:  print out time/date and status string
void spTimeMode(){
    if(ch == 0) ch = 24;
    Serial.print("T: ");
    if(ch < 10) Serial.print(F("0"));  Serial.print(ch,DEC); Serial.print(F(":"));
    if(cm < 10) Serial.print(F("0"));  Serial.print(cm,DEC); Serial.print(F(":"));
    if(cs < 10) Serial.print(F("0"));  Serial.print(cs,DEC);
    
    Serial.print(F("\tM"));            Serial.print(mode);
    Serial.print(F("\tS"));            Serial.print(lm);
   
    Serial.print(F("\tSEC h: "));      Serial.print(hue_se);
    Serial.print(F(", v: "));          Serial.print(val_se);
    
    Serial.print(F("\tMIN h: "));      Serial.print(hue_mn);
    Serial.print(F(", v: "));          Serial.print(val_mn);
    Serial.print(F(", f: "));          Serial.print(minChangeFlag);
   
    Serial.print(F("\tHOUR h: "));     Serial.print(hue_hr);
    Serial.print(F(", v: "));          Serial.print(val_hr);
    Serial.print(F(", f: "));          Serial.print(hrChangeFlag);

    Serial.print(F("\tOpt: "));        Serial.print(options);
    Serial.println(F(""));
    digitalWrite(led13, HIGH); delay(50); digitalWrite(led13, LOW);
}


void getTime() {
   now = RTC.now();                                  // get current time
   ch=now.hour(); cm=now.minute(); cs=now.second();
}

// ********************************************************************************** //
//                          LED DISPLAY - HUE AND BRIGHTNESS
// ********************************************************************************** //

// DISPLAY SECONDS: cycle through the LEDs, one second at a time with seconds hue and brightness values
void showLEDsecs(byte now_s) {              // 
  int vad_mn = int (val_se + (lm*val_se)/lmDiv);
  if(showSecs) leds[now_s] = CHSV(hue_se, 255, int (val_se + (lm*val_se)/lmDiv));
}

// DISPLAY MINUTES: illuminate/extinguish LEDs depending on odd/even hour 
// hour is even: All LEDS are off. LEDs are illuminated from 0 to 59. Each minute the next LED ramps from no to background brighness
// hour is odd:  All LEDS are on, and LEDs are extinguished from 0-59. Each minite the next LED ramps from background to no brighness
void showLEDmins(byte now_h, byte now_m, byte now_s) {  //
  int vad_mn = int (val_mn + (lm*val_mn)/lmDiv);        // adjust intensity to ambient light levels   
  for(byte i = 0; i < NUM_LEDS; i++) { 
    if(!(now_h%2)) {
       leds[i] = (i < now_m)? CHSV( hue_mn, 255, vad_mn ) : CHSV( 0, 0, 0); 
    } else {
       leds[i] = (i < now_m)? CHSV( 0, 0, 0) : CHSV( hue_mn, 255, vad_mn ); 
    }      
  }
 
  if(now_s == 0) {                                     // if just changed minutes
    minChangeFlag = true;                              // set flag
    val_mx = (!(now_h%2))? valMin : vad_mn;            // set starting brightness
    val_inc = max(2,vad_mn/50);                        // set brightness increment
  }

  if(minChangeFlag) {
    if(!(now_h%2)) {
       val_mx = min(vad_mn, val_mx + val_inc );        // ramp up current min LED from 0 to max... 
       if(val_mx >= vad_mn) minChangeFlag = false;     // ... and when there, reset flag
    } else {
       val_mx = max(valMin, val_mx - val_inc );        // ramp down current min LED to 0... 
       if(val_mx <= valMin) minChangeFlag = false;     // ... and when there, reset flag
    }
  }
  leds[now_m] = CHSV( hue_mn, 255, val_mx);            // set brightness level of current minute LED
}


// DISPLAY HOURS
void showLEDhrs(byte now_h, byte now_m, byte now_s) {
  now_h = now_h%24;                                    // 0-23 hours
  if(now_h >= 12) now_h = now_h - 12;                  // adjust to 12 hour clock

  int vad_hr = int (val_hr + (lm*val_hr)/lmDiv);       // adjust intensity to ambient light levels

  if( !hrChangeFlag && (((now_m+1)%60)/12 !=  now_m / 12) && now_s==30 ) {
     hrChangeFlag = true;                              // one minute to next hour LED (1/5 hour)
     val_hx = vad_hr;                                  // set brightness to decrement from
     val_hdec = val_hx/20;                             // set decrement value
  }
  
  byte now_hled = (now_h * 5); now_hled += (mode != mode_hrsadj)? now_m / 12 : 0;            // add additional LEDs for minutes past hour
  if(hrChangeFlag) {
    leds[now_hled] = CHSV( hue_hr, 255, val_hx);                       // fade down current hours LED
    leds[(now_hled+1)%60] = CHSV( hue_hr, 255, vad_hr - val_hx);       // fade up next hours LED
    val_hx = max(valMin, val_hx - val_hdec);                           // decrement brightness = 0
    if(val_hx == valMin && now_s == 59) hrChangeFlag = false;          // and once there, reset flag
  } else {
    leds[now_hled] = CHSV( hue_hr, 255, vad_hr );                      // normal mode
  }
}


// ********************************************************************************** //
//                          HUE AND BRIGHTNESS ADJUSTMENTS
// ********************************************************************************** //

void adjColour() {
  if(mode == mode_hrshue) {        // adjust hours hue
    if(clockwise) { if(++hue_hr > hueMax) hue_hr = hueMin; } else { if(--hue_hr < hueMin) hue_hr = hueMax; }
    saveSettings();
  }

  if(mode == mode_hrsval) {        // adjust hours max brightness (value)
    if(clockwise) { val_hr+=5; } else { val_hr-=5; }
    saveSettings();
  }
  
  if(mode == mode_minhue) {        // adjust minutes hue
    if(clockwise) { if(++hue_mn > hueMax) hue_mn = hueMin;  } else { if(--hue_mn < hueMin) hue_mn = hueMax;  }
    saveSettings();
  }
  
  if(mode == mode_minval) {       // adjust minutes background brightness (value)
    if(clockwise) { val_mn+=5; if(val_mn > valMax) val_mn = valMin;  } else { val_mn-=5; if(val_mn < valMin) val_mn = valMax; }
    saveSettings();
  } 

  if(mode == mode_sechue) {        // adjust seconds hue
    if(clockwise) { if(++hue_se > hueMax) hue_se = hueMin;  } else { if(--hue_se < hueMin) hue_se = hueMax;  }
    saveSettings();
  }
  
  if(mode == mode_secval) {       // adjust seconds brightness (value)
    if(clockwise) { val_se+=5; } else { val_se-=5; }
    saveSettings();
  } 

  if(mode == mode_optadj) {       // option adjust
    if(clockwise) { options = (++options)%4;  } else { options = (--options)%4;}
    saveSettings();
  } 

}

// function adjTime(): increments/decrements (based encoder direction) hours/mins/secs (depending on mode)
void adjTime(DateTime now, boolean dir) {
  
  if(mode == mode_hrsadj) {    // adjust hours
    byte adj_hrs = now.hour();
    if(dir) {        // increment
       if(++adj_hrs >= 25) adj_hrs = 1;
    } else {         // decrement
      if(adj_hrs == 0) adj_hrs = 24;
      if(--adj_hrs <= 0) adj_hrs = 24;
    }
    RTC.adjust(DateTime(now.year(), now.month(), now.day(), adj_hrs, now.minute(), now.second() ));
  } 
  
  if(mode == mode_minadj) {    // adjust minutes
    byte adj_mins = now.minute();
    if(dir) {   
      if(++adj_mins >= 60) adj_mins = 0; 
    } else {
      if(adj_mins == 0) adj_mins = 60; 
      --adj_mins;
    }
    RTC.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), adj_mins, now.second() ));
  } 

  if(mode == mode_secadj) {    // adjust seconds
    if(dir) { 
      RTC.adjust(DateTime(now.year(), now.month(), now.day(), now.hour(), now.minute(), 0 )); 
    } 
  } 

}

void doModeLEDs() {
  byte x;
  switch(mode) {
    case mode_normal:  break;                                              // normal mode
    
    case mode_hrshue:                                                     // set all hour LEDS to hour hue
      for(x = 0; x<NUM_LEDS; x++) { leds[x] = ( !(x%5) )? CHSV(hue_hr, 255, 255 ) : CHSV(0,0,0); }  
      break;

    case mode_hrsval:                                                     // hour intensity adjust (dim)
      for(x = 0; x<NUM_LEDS; x++) { leds[x] = ( !(x%5) )? CHSV(hue_hr, 255, val_hr ) : CHSV(0,0,0); }  
      break;
     
    case mode_hrsadj:                                                      // hour time adjust
      for(x = 0; x<NUM_LEDS; x++) { 
        leds[x] = (x == (ch%12)*5 )? CHSV(hue_hr, 255, (155+100*(cs%2)) ) : CHSV(0,0,0); 
      }
      break;      
    
    case mode_minhue:                                                    // minute hue adjust
      for(x = 0; x<NUM_LEDS; x++) { leds[x] = (!(x%3))? CHSV( hue_mn, 255, 255 ) : CHSV(0,0,0); }   
      break;
      
    case mode_minval:                                                    // minute background intensity adjust (dim)
      for(x = 0; x<NUM_LEDS; x++) { leds[x] = (!(x%3))? CHSV( hue_mn, 255, val_mn ) : CHSV(0,0,0); }
      break;

    case mode_minadj:                                                      // minute time adjust
      for(x = 0; x<NUM_LEDS; x++) { 
        leds[x] = (x == cm)? CHSV(hue_mn, 255, (155+100*(cs%2)) ) : CHSV(0,0,0); }
      break;

    case mode_sechue: // second hue adjust
      for(x = 0; x<NUM_LEDS; x++) { leds[x] = (!(x%6))? CHSV( hue_se, 255, 255) : CHSV(0,0,0); }  
      break;     

    case mode_secval: // second intensity adjust (dim)
      for(x = 0; x<NUM_LEDS; x++) { leds[x] = (!(x%6))? CHSV( hue_se, 255, val_se) : CHSV(0,0,0); }  
      break;     

    case mode_secadj: // second intensity adjust (dim)
      for(x = 0; x<NUM_LEDS; x++) { leds[x] = (x == cs)? CHSV( hue_se, 255, 255) : CHSV(0,0,0); }  
      break;     
  }
}

void doRunningSecond() {
   if(!runSecs)  return;      // return if function not enabled
   byte l=cs;
   for(byte x = 0; x<NUM_LEDS; x++) {
     if(++l >= 60) l=0;
     byte tr = leds[l].red, tg = leds[l].green, tb = leds[l].blue;
     leds[l] = CHSV(hue_se + (x*0), 255, 80);
     FastLED.show();
     delay(16);
     leds[l] = CRGB(tr, tg, tb);
   }
}


void doLightSense() {
   lm = map( constrain(analogRead(lsPin),lsMin,lsMax),lsMin,lsMax,lmMin,lmMax );
}


// ********************************************************************************** //
//                              EEPROM MANAGEMENT
// ********************************************************************************** //

void saveSettings()    { for(int addr = 0; addr < sizeof(set)/2; addr++) { EEPROM.write(addr, *set[addr]); } }    // save

void restoreSettings() {               // restore settings
  boolean doDef = true;                // if no settings have been previously stored, flag is true 
  for(int addr = 0; addr < sizeof(set)/2; addr++) { 
    *set[addr] = EEPROM.read(addr);    // recover EEPROM values
    if( EEPROM.read(addr) != 0xFF ) doDef = false;  // if at least one setting is not 0xFF then reset flag
  } 
  if(doDef) setDefaults();            // however, if no settings have been set then set defaults.
}  

void setDefaults()     { for(int addr = 0; addr < sizeof(set)/2; addr++) { *set[addr] = def[addr]; } }            // set defaults


// ********************************************************************************** //
//                                      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; 
}


void doOptions() {
  showSecs = bitRead(options,showSecsb);        // show second flag
  runSecs  = bitRead(options,runSecsb);         // run seconds flag 
}

[/codesyntax]

One thought on “Wall Clock

  1. Gurjinder

    I liked the clock please send the schematic for circuit or Rotary Encoder pins image

    Thanks

    Reply

Come on... leave a comment...