The Giving Tree and An Arduino Clock

When we bought our house we hired an electrician to install ceiling fans and lights. He worked, worked, and worked. My illusions of electrician’s work dissolved. Their work is a great deal more like laying bricks than solving equations. Oddly, the truth for electricians is also the truth for electronics.

Nearly two years after starting, more than 18~months after finishing the electronics, source code, and cardboard mock-up I finally finished my clock. I have recreated—nay, improved upon—the greatest alarm clock I have ever owned.

20120804-Pano-Backyard2

In some ways my clock was started about thirty years ago, when some foresighted person planted a lovely little apple tree in what is now my back yard. The lovely little apple tree was a gorgeous mature giant shady apple tree until the spring of 2012. The spring came warm and early but also trisected by two severe cold spells. The frost killed several of my trees major limbs, and may eventually kill the tree.

One of the major limbs became firewood. Then one of the firewood logs became boards, and finally one of the boards became the main faces of the clock. Making a board from firewood is, no doubt, ancient. For me it was a new experience with old techniques. I used a hand powered bow saw, affixed the firewood in the vice, and ripped boards by hand. It is a tiresome process and the boards were not machined to the parallel faces you get in dimensioned lumber. No matter, hand planes helped me get one face smooth and flat. I marked a constant thickness, flipped the board over, and planed the uneven face. The finished board was smooth, clean, and beautifully figured.

The original vision for the box of the clock had hand-cut dovetails visible on the front. However, the apple wood was unsupportive. It was brittle, and prone to fracturing on detail peices. It was also almost too small. The cherry boards I selected for the sides were not long enough to dovetail join the front and back.

clock_hand_drawing

The final result is lovely to me. It has a lid so the interface is not visible on the bedside, and the lid showcases the figure of the apple wood. The display is a simple red that does not keep me awake at night. The plain box design weighs enough to keep from migrating around the nightstand, and the interface is actually good.

20140817-0620140817-0120140817-0520140817-02-2

Design and Use

I set out to imitate the functions of my beloved but deceased clock, see Behold! A number pad. I started the design with a set of use cases, Set Time, Set Alarm, Turn on Alarm, and Turn off Alarm. The notes below are from the original design before I bought the first part, with no edits but formatting.

Set Time

User opens decorative lid. User sets mode switch “set time” mode. The display is turned to current time. User enters numbers starting with most significant digit, in 24 hour time format. After entering the first number, only that digit is displayed.

If user enters a mistake, he hits “clear (#),” and the process restarts at “user enters numbers”. Clear: display is blanked. To keep the setting, user leaves the “set time” mode. On error, the time remains unchanged. Error conditions are:

  • User exits "set time" mode without entering a new time at all.
  • User exits "set time" mode without entering a valid time.

Set Alarm

Same as “Set Time,” but using “set alarm” mode.

Turn on Alarm

User turns on alarm. Display flashes time the alarm is set for about 5 seconds. Display resumes normal operation. The “alarm on” LED lights. When the time reaches the set alarm time, a beeper will sound. The beeper will sound until the alarm is shut off.

Turn off Alarm

Alarm LED is turned off. If the alarm is currently beeping, the beeping ceases.

Electronics Design

I ended up with a Sparkfun keypad, and care of Andrew’s diligent work I was able to configure the Arduino microprocessor to use the keypad almost painlessly. I arranged the pinout as follows:

Pin Use
0 not available
1 not available
2 keypad 0
3 keypad 1
4 keypad 2
5 keypad 3
6 keypad 4
7 keypad 5
8 keypad 6
9 alarm on/off switch
10 alarm tone (PWM)
11  
12  
13  
14 (A0)  
15 (A1) display brightness potentiometer
16 (A2)  
17 (A3) mode switch (3-state) alarm/time
18 (A4) i2c – display and realtime clock
19 (A5) i2c – display and realtime clock

Pin A3 was used for analog read to determine the state of a simple resistor network. Switching the mode switch changes the resistors in a voltage divider. I designed this before I learned about the Arduino’s internal pull-up resistor, so the analog electronics are more difficult than I would make them today.

In case anyone wants it, the code is here also.


#include <Wire.h>
#include "Adafruit_LEDBackpack.h"
#include "Adafruit_GFX.h"
#include <Keypad.h>
#include "RTClib.h"

RTC_DS1307 RTC;
DateTime now;

#define PIN_ALARMSWITCH 9
#define PIN_ALARMTONE 10
#define PIN_MODE3STATE A3

#define PIN_BRIGHTNESSPOT A2 // on the left

#define COLON 2
#define PM 8
#define ALARM 4
#define EXTRA 16

int brightness( void){
  // the analog reading is between 0 and 1024, so divide by 64 to get the
  // values between 0 and 15
  return analogRead( PIN_BRIGHTNESSPOT) >> 6; 
}

class ClockDisp : public Adafruit_7segment {
  uint8_t alarmOn;
  uint8_t pmOn;
  uint8_t colonOn;
  public:
  void writeState( void);
  void alarm( bool);
  void pm( bool);
  void colon( bool);
  void toggleColon( void);
  void printTime( DateTime now);
  void blank( void);
};

void ClockDisp::blank( void){
  print(10000, DEC);
  writeDisplay();
}

void ClockDisp::writeState( void){
  writeDigitRaw( 2, alarmOn | pmOn | colonOn );
  writeDisplay();
}

void ClockDisp::alarm( bool newState ){
  if( newState){
    alarmOn = 4;
  }else{
    alarmOn = 0;
  }
  writeState();
}

void ClockDisp::pm( bool newState){
  if( newState){
    pmOn = 8;
  }else{
    pmOn = 0;
  }
  writeState();
}

void ClockDisp::colon( bool newState){
  if( newState){
    colonOn = 2;
  }else{
    colonOn = 0;
  }
  writeState();
}

void ClockDisp::toggleColon( void){
  colonOn ^= 2;
  writeState();
}

void ClockDisp::printTime( DateTime now){
  //DateTime now = RTC.now();
  int hour = now.hour();
  if( hour > 12){
    pmOn = 8;
    hour %= 12;
  } else{
   pmOn = 0;
   if( hour == 0) hour = 12;
  }
  int decimalTime = hour * 100 + now.minute();
  print( decimalTime);
  writeState();
}        
        
const byte ROWS = 4; //four rows
const byte COLS = 3; //four columns
//define the cymbols on the buttons of the keypads
char keys[ROWS][COLS] = {
  {'1','2','3'},
  {'4','5','6'},
  {'7','8','9'},
  {'*','0','#'}
};
byte rowPins[ROWS] = {7,2,3,5}; //connect to the row pinouts of the keypad
byte colPins[COLS] = {6,8,4}; //connect to the column pinouts of the keypad

//initialize an instance of class NewKeypad
Keypad customKeypad = Keypad( makeKeymap(keys), rowPins, colPins, ROWS, COLS); 

//Adafruit_7segment matrix = Adafruit_7segment();
ClockDisp clockDisp = ClockDisp();
int alarmNum = -1;
bool isAlarming = 0;
bool hasAlarmed = 0;

void setup(){

  Serial.begin(9600);
  Serial.println("Clockit");
  
  Wire.begin();
  RTC.begin();

  if (! RTC.isrunning()) {
    Serial.println("RTC is NOT running!");
    // following line sets the RTC to the date & time this 
    // sketch was compiled, except on Mac.
    RTC.adjust(DateTime(__DATE__, __TIME__));
  }
  
  clockDisp.begin(0x70);
  clockDisp.setBrightness( 10);
  
  clockDisp.blank();
}

void loop() {
  Serial.println("Top of loop");
  now = RTC.now();
  Serial.print("Brightness pin: ");
  Serial.println( analogRead( PIN_BRIGHTNESSPOT));
  clockDisp.setBrightness( brightness());
  // Print the time, and update the colon
  Serial.print("Time decimal: "); Serial.println( now.hour() * 100 + now.minute());
  clockDisp.printTime( now);
  if( millis()%1000 < 500){
    clockDisp.colon(1);
  }else{
    clockDisp.colon(0);
  }
  delay( 20);
  
  /* The alarm function is actually a little bit complicated. It is
  easy to test if the current time is equal to the alarm time. We 
  want the alarm to start beeping and not to stop until the switch 
  is thrown. However, if we immediately turned the switch back on,
  the current time would still exceed the alarm time, and it would
  start beebing. Therefore, I created the hasAlarmed variable, which
  records that the alarm has gone off. hasAlarmed will reset to FALSE
  when the time is less than the alarm time, indicated that we've come
  around the clock again.
  */
  
  if( digitalRead( PIN_ALARMSWITCH)){
    clockDisp.alarm( 1);
    if( isAlarming){
      if( millis()%1000 < 500){
        tone( PIN_ALARMTONE, 1000);
      }else{
        noTone( PIN_ALARMTONE);
      }
    } else if( hasAlarmed){
      if( (now.hour() * 100 + now.minute()) < alarmNum ){
        hasAlarmed = 0;
      }
    } else {
      // See if we need to turn on the alarm
      if((now.hour() * 100 + now.minute()) == alarmNum){
        isAlarming = 1;
      }
    }
  }else if( isAlarming){
    clockDisp.alarm( 0);
    noTone( PIN_ALARMTONE);
    isAlarming = 0;
    hasAlarmed = 1;
  }else{
    clockDisp.alarm(0);
  }
 
  
   
   // Contol of setting state switch
   int mode = analogRead( A3);
   Serial.print("Mode reading: "); Serial.println( mode);
   if( mode > 900 ){ // set the time
    Serial.println("Setting the time");
    // blank the display  
    clockDisp.blank();
    int numDigits = 0;
    int timeNum = 0;
    
    while( analogRead(A3) > 900){
      // wait for keys, display them, check state
      // wait for entry of time
      char customKey = customKeypad.getKey();
      if( customKey > 0){
        if( numDigits > 0){
          timeNum = timeNum*10 + (int)(customKey - '0');
        }else{
          timeNum = (int)(customKey - '0');
        }  
        numDigits ++;  
        
        clockDisp.print( timeNum);
        clockDisp.writeDisplay();
      }
    }
    // Test to see if we got a valid time:
    if( (timeNum < 2500) && (timeNum > 0)){
      RTC.adjust( DateTime( now.year(), now.month(), now.day(),
              timeNum / 100, timeNum%100, 0));
    }else{
      // An error, flash bars at the user
      clockDisp.print( 10000);
      clockDisp.writeDisplay();
      delay( 300);
    }
  }else if( mode > 490){ // state 2
    clockDisp.print( alarmNum);
    clockDisp.writeDisplay();
    int numDigits = 0;
   
    while( 1 ){
      int ar = analogRead( A3);
      if( (ar < 490) || (ar > 900)) break;

      // wait for keys, display them, check state
      // wait for entry of time
      char customKey = customKeypad.getKey();
      if( customKey > 0){
        if( numDigits > 0){
          alarmNum = alarmNum*10 + (int)(customKey - '0');
        }else{
          alarmNum = (int)(customKey - '0');
        }  
        numDigits ++;  
        
        //clockDisp.print( (int)(customKey - '0'));
        clockDisp.print( alarmNum);
        clockDisp.writeDisplay();
      }
    }
    // Test to see if we got a valid time:
    if( (alarmNum >= 2500) || (alarmNum < 0)){
      // Invalid time entered, flash error bars at user
      clockDisp.print( 10000);
      clockDisp.writeDisplay();
      delay( 300);
    } else{
      hasAlarmed = 0;
    }
  } 
}

int getDecimalTime(){
  DateTime now = RTC.now();
  int decimalTime = now.hour() * 100 + now.minute();
  return decimalTime;
}