Clock Project

Here is my Clepsydra project, also known as "Mom's Clock".  It's based on 3,700-year-old water clocks, in this case using 1" steel balls to tell the time.  For example, at 3:00:00pm three balls roll out and skitter noisily across the floor. This project includes 13 ball bearings, 1,474 lines of Arduino C++ code, and several feet of wire.

The two parts of the clock are the control box and the raceway. The menu-driven control box contains all the electronics, and the V-shaped raceway has a servo-controlled ball release mechanism. The control box and raceway are connected by a simple stereo cable.

 

 

Software

You may set alarms for any time between now and December 31st 2099, such as one’s nth birthday, announced by your choice of 1 to 13 balls, with an optional repeat time, in this case one year.  This clock also can use ship’s bells to announce the half-hours in each 4- or 8-hour shift, or choose its own random times just for fun.  There’s an optional "quiet time", by default 10pm to 5am, when no balls are released.

See some sample C++ code.

 

Control Box

These pictures show the control electronics before (components), during (soldered together), and after (in the box!).

 

Clock -- components    

Components, row by row:  
stereo cable, on/off switch and key, battery,
knob and rotary switch, battery charger,
Arduino Nano,
real-time clock with its own battery, and a two-line display with an RGB backlight.

 

  Clock -- soldered     

Here is everything wired together, with all components in their sockets. The kludge board (upper left, beige) is a rectifier circuit to prevent current backflow from the battery through the Nano (correcting a rare design flaw).

 

Clock!

Finally assembled box!  The rotary switch brings up menu items and values, with click-to-select.

 

Raceway

The raceway is basically an inclined plane where each ball's potential energy is efficiently converted into kinetic energy with a minimum of friction.  The height of the release gate can be changed, and the default 4" is sufficient for at least 10' of rolling across a hard floor.

 

Sample Arduino C++ Code

 Here is a sample of the code, all of which I wrote, with some help from StackExchange & Google:  

  1. All alarms are represented in an array myTimes as defined by the timeElement structure.
  2. The checkSetTimes procedure runs every 333 milliseconds and checks whether the current second is equal to or past any non-empty alarm times, and if so, and we’re not in quiet time, rolls out the specified number of balls for that alarm type.  Any subsequent time for that alarm is calculated and stored, such as the next bell time.
  3. The “Existing Timers” menu selection invokes the showTimes procedure to display the currently set alarms’ types and times – you scroll through them by rotating the switch, and exit with a click.  This procedure also calls a timeout function which returns true for 12 seconds after your most recent switch change, so the procedure will eventually exit after you stop scrolling.  Since you may be fiddling with the switch for more than one second, showTimes calls checkSetTimes each time through its loop to ensure all alarms are seen.

 

enum timeTypes {
  tempty = 0, talarm, trepeat, trandom, tbells, tquietude
};
typedef struct timeElement {
  timeTypes xTimeType = tempty;
  DateTime xTime = tZero;
  TimeSpan xTS = tsZero;
}; 
timeElement myTimes[maxTimes];

void checkSetTimes() {
  boolean oneOfUs = false;   
  boolean rollable = false;  

  for (int j = 0; j < maxTimes; j++) {

    oneOfUs =                           // is this an active alarm type?
(myTimes[j].xTimeType == talarm ) || 
(myTimes[j].xTimeType == trepeat ) || 
(myTimes[j].xTimeType == trandom) || 
(myTimes[j].xTimeType == tbells);

 
    if (oneOfUs) {
      if (checkForNow(myTimes[j])) {        // is it time for this alarm?
        rollable = !inTimeout(myTimes[j]);  // are we in quiet time?
        switch (myTimes[j].xTimeType) {
          case talarm:
            sendBalls(myTimes[j].xTS.totalseconds());
            myTimes[j].xTimeType = tempty;
            break;
          case trepeat:
            if (rollable) {
              sendOneBall("Repeat");
            }
            myTimes[j].xTime = myTimes[j].xTime + myTimes[j].xTS;
            break;
          case trandom:
            if (rollable) {
              sendOneBall("Random");
            }
            myTimes[j].xTime = DateTime(myTimes[j].xTime.unixtime() + random(rMinMinute, rMaxMinute) * 60);  
            break;
          case tbells:
            if (rollable) {
              sendBellBalls(myTimes[j].xTS.totalseconds());
            }
            myTimes[j].xTime = DateTime(myTimes[j].xTime.unixtime() + halfHour);
            myTimes[j].xTS = ((myTimes[j].xTS.totalseconds() + 1) % numBells) + 1;
            break;
          default:
            if (rollable) {
              sendOneBall("Default");
            }
        }

      }
    }
  }
}

char* menuStrings[MENUSIZE] = {"Just the Clock", "Reminder", "Repeat", "Random", "Bells", "Quiet Time", "Settings", "Existing Timers"};

void showTimes() {
  boolean done = false;
  boolean foundOne = false;

  while (!done && !timeout()) {

    checkSetTimes();

    if (count != old_count) {             // the switch was rotated
      inc = (count > old_count) ? 1 : -1;
      old_count = count;
      updateCurIndex(inc);
    }

    foundOne = false;
    for (int i = 0; i < maxTimes; i++) {
      if (myTimes[i].xTimeType != tempty) {
        foundOne = true;
        break;
      }
    }

    if (foundOne) {
      if (myTimes[curIndex].xTimeType != tempty) {
        // display the current alarm type & time 
        showEvent(myTimes[curIndex]);  
        do {
          checkSetTimes();
          if (captureSwitchState()) {     // the switch was clicked
            justClick = shortClick = longClick = false;
            done = true;
          }
        } while ((count == old_count) && !timeout() && !done);
      } else {
        updateCurIndex(inc);
      }
    } else {
      lcd.setCursor(0, 1);
      lcd.print("No timers set!");
      delay(comprehensionTime);
      done = true;
    }
  }
}