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.
Control Box
These pictures show the control electronics before (components), during (soldered together), and after (in the box!).
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.
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).
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:
- All alarms are represented in an array
myTimes
as defined by thetimeElement
structure. - 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. - 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 atimeout
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
callscheckSetTimes
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; } } }