The Intro:

This Exercise Clock was created as a final project submission for CMU Ideate’s Physical Computing class. Our prompt was to meet with an older person in our community (selected for us from CMU’s OSHER classes) and create an assistive device personalized to them.

Our group was paired with Jan, and through our previous discussions with her, we learned that she enjoyed going on walks, but felt that she wasn’t as active as she should be. Since she didn’t care for the exercise apps she had tried, we created a prototype clock for her to use to track her physical activity.

We then finalized the project with this version, according to Jan’s feedback to the prototype.

The Product:

Our final product is a clock that reminds Jan to exercise and keeps track of how long she exercises. The clock cycles between three screens,  the first displays the time and date, the second displays bar graph of how long Jan exercised each day for the past two weeks, and the third compares the time Jan spent exercising the current week and last week. There is also a row of LEDs that display how many days from the past week that Jan has exercised. If Jan hasn’t met her exercise goal for the day, the clock will beep every hour if it senses someone in the room. When the button is pressed, the screen displays a timer.  When the button is pressed again, the timer is turned off and the time is added to the graph and if the exercise goal is met, one of the LEDs turns on. There is also a mute switch that puts makes the screen dim and makes the clock stop beeping.

Overall Photo:

An overall photo of our final product.

Basic Operation:

Pressing the button to start and stop the timer.

Twisting the button to set the time.

Flipping the mute switch on and off.

Detail Photos:

The display showing the time and date.

The display showing the graph of time spent exercising for the past two weeks.

The display comparing the amount of exercise this week and last week.

The display when the clock is in muted mode.

The LED row with one LED on, and the motion sensor.

The speaker and the off switch.

Usage Photos:

Pressing the button to stop the timer.

Turning the button to set the time and date.

Flipping the switch and putting the clock in muted mode.

The Process:

Following the formative critique, we created a schedule to guide us through the remainder of the project.

After completing our prototype we created this plan to finish the project.

Our first tasks were to complete the functionality of the device, and we were able to stay on schedule while doing so.

We wanted Jan to be able to view her recent workout history from a distance.   Our group decided to use a row of 7 Red-Green LED’s on our reminder clock to display the information in an manner that was easily noticeable but not out of place in her kitchen.  Each Red-Green LED would represent a day in the past week, appearing green if she worked out but red otherwise.  The easiest way to attach these lights would be with one pin per color for every light, which would use 14 pins.   However, due to the limited number of outputs on the Arduino we needed to condense this feature into just 7 pins.  To achieve this goal, we took advantage of each light only needing to be red or green at a certain time and used transistors to select between the two options with a single pin.   We also ran into early problems powering all 7 Red-Green LEDs at once, but using the 5v pin as the main power source for the lights fixed this problem for the time being.

Initial ideas for reducing the number of pins needed for the LEDs.

Our final circuit for the Red-Green LEDs.

Furthermore, Jan mentioned multiple features that she believed would improve her experience with the device.  She worried that the clock would continue to provide alerts while she was out of town with her neighbor looking after the house.  Our group decided to include a mute switch which dimmed the screen and stopped alerts in this situation.  She also expressed multiple concerns about the longevity of the device, so we decided on several changes to improve this area.  We added a more precise time keeping element, so the clock would skew less overtime.  In software, we fixed bugs that would occur when the date had an overflow error.  Also, for any unexpected errors ,as well as daylight savings time and leap years, we added the ability to change the time and date manually.  This feature required a new input to choose between times, so we changed the button on the device into a rotary encoder to increase the functionality while keeping the interface minimal.

The rotary encoder we replaced our button with to enable the time changing functionality.

A test of changes we made to our code to prevent overflow errors.

After completing the functionality of the reminder clock we began to assemble the final product, but the construction took longer then we anticipated.  We originally planned to finish before Thanksgiving break, but our group had to do considerable work after we returned.  This delay was largely due to us underestimating the work needed to finish the case.  When initially designing the outside, we combined math and intuition to create an overall shape.  We wanted the device to be large enough to sit between two wall trims without leaving too much space, and the top to be at an angle that she could easily view while standing.

A sketch used in deciding the angle of the clock’s top.

Our first setback in manufacturing occurred when choosing how to assemble the case.  We originally designed a file to 3d print, but afterwards decided to change to laser engraving due to size constraints on the 3d printer.  When we tried to assemble our final box our group faced multiple other issues.  We decided to use a “5 minute” epoxy to connect the pieces.  However, we did not mix the components correctly, which created a messy layer of residue on our product.  This initial failure made later attempts to glue box less effective, as the original epoxy blocked the new adhesive.  Later, when putting components into the box, we accidentally shattered one of the sides.  These two errors led to us re-engraving the case.  The second time was much neater, as we were more careful and used acrylic glue.

A cardboard prototype of our box used to optimize hole sizes before our final print.

A shattered side of our box after a panel mounting accident.

Unfortunately, when assembling the final circuitry using an Arduino nano we ran into a problem powering all 7 LEDs similar to our earlier issue.  This time, however, our solution of using the 5v pin as the main power source for the lights was not working.  Since we had fallen behind schedule, we did not have time to solve this problem, so we decided to replace the Red-Green LEDs with normal LEDs for our final clock.

The Red-Green LEDs were replaced with simple blue LEDs due to problems when making the final circuitry.

The Feedback:

Our project received mostly positive feedback, though, some valid criticisms and suggestions were raised. One commenter noted, for example, that

“LED lights + day of week on display should go in same direction”

in reference to the fact that our LED display on the front of the device represented the most recent day on the left of the device, while our bargraph on the LCD had  the most recent day on the right. This is an aspect of the device we hadn’t noticed, however changing it did make the different displays of the device more intuitive to comprehend at a glance.

Another comment we received regarded the physical presence of the device:

“Consider making the knob a bit bigger – it might make it easier to use.”

As a group, we also agreed with this criticism, to the extent that we were originally planning to make the knob much larger than it is currently in our final project. While unfortunately we weren’t able to get the custom part created in time, Jan at least seemed pleased with the replacement knob we did end up using.

One of the most important things we learned while creating something for another person, is the importance of listening. Many of the design choices we made to customize this clock for Jan were the result of minor comments she made during the interview process. Things like the size of the device, and our method of interaction we as effective as they could be because we noted specifically how Jan expected to use the device.  If we were to do this project another time, we would most likely pay more attention to the physical for and aesthetics of the device earlier in the design process, to ensure we wouldn’t have another plastic box panic the night before the project was due.

If we were to do this project again, one of the most useful modifications to the design process would be to include some time for “beta testing.” While we were able to demonstrate a prototype of our project to Jan to get some basic feedback, the feedback we got was somewhat limited in that we collected it in a classroom setting. Being able to let Jan take the device home with her for a few days of testing could potentially bring light to some more minor issues which we wouldn’t be able to discover within a classroom environment.

The Details:

The electrical schematic of our project.

The Code:

 * Workout_Clock.ino
 * Exercise Reminder Clock
 * Karen Abruzzo, Justin Kiefel, George Ralph
 * This code runs a clock, which can be used to track the duration of
 * exercise intervals with stopwatch-like functionality whenever TIMER_BUT_PIN
 * is pressed. The clock also tracks exercise history over a 2 week period, 
 * and reminds the user through audio tones to exercise whenever they are 
 * detected by the motion sensor. (Though this feature can be disabled)
 * Inputs
 *    0   -   Motion Sensor
 *    2   -   Rotary Encoder Clock Pin
 *    3   -   Rotary Encoder Data Pin
 *    4   -   Rotary Encoder Button
 *    5   -   Power/Mute Switch
 * Outputs
 *    6     - Speaker Pin
 *    7..13 - Bargraph LEDs
 * Outputs
 * NOTE: This project must be compiled with compiler warnings disabled due to 
 *       aspects of the libraries we use, which are beyond our control.

#include <Encoder.h>
#include <Wire.h>
#include <DS3231M.h> //library for the rtc
#include <LiquidCrystal_I2C.h>
#include "pitches.h"

/* Create an LCD display object called "screen" with I2C address 0x27
   which is 20 columns wide and 4 rows tall. You can use any name you'd like. */
LiquidCrystal_I2C screen(0x27, 20, 4);
DS3231M_Class DS3231M;

//Make buffer size the size of the screen area, just to be safe
const uint8_t  SPRINTF_BUFFER_SIZE = 80;
char inputBuffer[SPRINTF_BUFFER_SIZE];

const uint32_t SERIAL_SPEED = 115200;
const int MOTION_PIN         = 0;
const int ENC_CLK_PIN        = 2;
const int ENC_DT_PIN         = 3;
const int TIMER_BUT_PIN      = 4;
const int POWER_SWITCH       = 5;
const int SPEAKER_PIN        = 6;
const int LED_BASE_PIN       = 7;

const int LED_CNT = 7;

//Goal time is 40 minutes
const int GOAL_TIME = 60*40;

Encoder enc(ENC_CLK_PIN, ENC_DT_PIN);

const char weekLetters[] = {'S', 'M', 'T', 'W', 'H', 'F', 'S'};
const String weekDays[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};

// 2 = half note, 4 = quarter note, ect.

int openSound[] {2, 8, 8, NOTE_B3, NOTE_B4};
int workoutSound[] {2, 8, 16, NOTE_B3, NOTE_B5};
int clockSound[] {1, 8, NOTE_A5};
int sadSound[] {2, 4, 4, NOTE_F3, NOTE_C3};
int happySound[] {4, 8, 8, 8, 8, NOTE_C4, NOTE_E4, NOTE_G4, NOTE_C5};

unsigned long start;
bool timerOn = 0;
int pastTimes[28];
int timeSeconds;
boolean isMidnight = false;

long lastPing = 0;
long pingTimeout = 3600;

bool advancePast = false;
bool timerPast = false;
long updateTime, buttonDownTime;
int screenState = 0;

bool pastPowerState = true;
bool powerState = true;

void setup() {
  // Initialize pins

  digitalWrite(SPEAKER_PIN, LOW);
  //Initialize the LED pins as a consecutive block of pins
  for(int i = 0; i < LED_CNT; i++) {
    pinMode(LED_BASE_PIN + i, OUTPUT);
  //setup pins

  while (!DS3231M.begin())
    //Serial.println(F("Unable to find DS3231MM. Checking again in 3s."));

  // initialize the screen (only need to do this once)
  powerState = !digitalRead(POWER_SWITCH);
  pastPowerState = powerState;
  DateTime now =;
  lastPing = now.unixtime();

  updateTime = millis();


void loop() {
  bool timerState = !digitalRead(TIMER_BUT_PIN);
  bool powerState = !digitalRead(POWER_SWITCH);

  //On button down
  if (timerState && !timerPast) {
    buttonDownTime = millis();

  //On button up
  if (!timerState && timerPast) {
    //If the button has been down for more than a second
    //Enter settings dialog
    if(buttonDownTime + 1000 <= millis()) {
    //Otherwise, start the timer
    updateTime = millis() - 3000;

  //In timer mode, draw the elapsed time so far
  if (timerOn) {
    //But only, once a second
    if ((updateTime + 1000) <= millis()) {
      updateTime = millis();
    if(powerState) {
      //Cycle the screen every 3 seconds when power is on
      if ((updateTime + 3000) <= millis()) {
        switch (screenState) {
          case 0:
          case 1:
            drawWeekCompare(getTotalTime(14, 21), getTotalTime(21, 28));
          case 2:
        updateTime = millis();
        screenState = (screenState + 1) % 3;  
      //Update the clock every few seconds when power is off
      if ((updateTime + 5000) <= millis()) {
        updateTime = millis();

    //If no time is logged for today (and device is on)
    if (pastTimes[27] == 0 && powerState) {
      //If the motion sensor detects motion
      if (digitalRead(MOTION_PIN)) {
        //If it's been a while since we last pinged
        DateTime now =;
        if (now.unixtime() >= (lastPing + pingTimeout)) {
          //Ping the user
          lastPing = now.unixtime();

  if (isNewDay() && !isMidnight)
    isMidnight = true;

  //Turn the screen on/off if the power switch is flipped
  if (powerState != pastPowerState) {
    if (powerState) {
      updateTime = millis();
    else {
      screenState = 0;


  pastPowerState = powerState;
  timerPast = timerState;

/* The time-setting dialog menu */
void setTime() {

  //Grab the current time
  DateTime now =;
  int hour = now.hour();
  int minute = now.minute();
  int day =;
  int month = now.month();
  int year = now.year();
  //Initialize buffers to print before and after the number we choose
  char beforeBuffer[SPRINTF_BUFFER_SIZE];
  char afterBuffer[SPRINTF_BUFFER_SIZE];

  char* header = "      Set Time      ";

  //For each time, format the string around it with the previously set time
  sprintf(beforeBuffer, "%s (", header);
  sprintf(afterBuffer, "):%02d %d/%02d/%04d", minute, month, day, year);
  hour   = getNumInput(24,    beforeBuffer, afterBuffer,   hour, 0);
  sprintf(beforeBuffer, "%s %2d:(", header, hour);
  sprintf(afterBuffer, ") %d/%02d/%04d", month, day, year);
  minute = getNumInput(60,    beforeBuffer, afterBuffer, minute, 0);

  sprintf(beforeBuffer, "%s %2d:%02d (", header, hour, minute);
  sprintf(afterBuffer, ")/%02d/%04d", day, year);
  month  = getNumInput(12,    beforeBuffer, afterBuffer,  month, 1);

  sprintf(beforeBuffer, "%s %2d:%02d %2d/(", header, hour, minute, month);
  sprintf(afterBuffer, ")/%04d", year);
  day    = getNumInput(31,    beforeBuffer, afterBuffer,    day, 1);
  sprintf(beforeBuffer, "%s %2d:%02d %2d/%02d/(", header, hour, minute, month, day);
  sprintf(afterBuffer, ")");
  year   = getNumInput(10000, beforeBuffer, afterBuffer,   year, 0);

  DateTime newTime = DateTime(year, month, day, hour, minute, 0);

  //Make sure we show the clock screen after this
  screenState = 0;

/* Creates a number input dialog, with strings before and after the number 
   An initial value can be specified, 
   Input is modular about bound (ie 60 for values 0-59)
   The bound range can be offset via minOffset (ie 1 for 1-60 in the above example) */
int getNumInput(int bound, String before, String after, int initial, int minOffset) {
  int value = 0;
  int offset = / -4;
  //Make the screen update at least once when we enter this dialog
  bool forceDraw = true;

  while (digitalRead(TIMER_BUT_PIN)) {
    int pos, oldPos;
    pos = ( / -4) - offset + (initial - minOffset);
    if (pos != oldPos || forceDraw) {
      value = ((pos + bound) % bound) + minOffset;

      sprintf(inputBuffer, "%2d", value);
      oldPos = pos;
      forceDraw = false;

  //Wait for the user to release the button and debounce
  while (!digitalRead(TIMER_BUT_PIN));

  return value;

/* Draws the default clock face screen */
void drawHome() {

  screen.setCursor(7, 1);
  DateTime now =;

  int hr12 = (now.hour() % 12);
  hr12 = (hr12 == 0) ? 12 : hr12;

  sprintf(inputBuffer, "%d:%02d%c", hr12, now.minute(), now.hour() >= 12 ? 'p' : 'a');
  screen.setCursor(1, 2);


  screen.print(weekDays[DS3231M.weekdayRead() % 7]);
  sprintf(inputBuffer, " %d/%d/%02d", now.month(),, now.year() % 100);

/* Draws the timer screen for when an exercise interval is in progress */
void drawTimer() {
  screen.setCursor(4, 1);
  screen.print("Elapsed Time");
  screen.setCursor(6, 2);

  long unixtime = (;
  long elapsed = unixtime - start;

  int seconds = elapsed % 60;
  elapsed -= seconds;
  int minutes = (elapsed / 60) % 60;
  elapsed = (elapsed / 60) - minutes;
  int hours = elapsed / 60;

  sprintf(inputBuffer, " %d:%02d:%02d", hours, minutes, seconds);


/* Draws the the 14 day bargraph screen (for during idle mode) */
void drawTwoWeekGraph(int values[]) {
  int weekDay = DS3231M.weekdayRead();

  for (int i = 0; i < 14; i++) {
    drawGraphBar(values[14 + i] / 225, 1, i + 3, 2);

  screen.setCursor(3, 3);
  for (int i = 1; i < 15; i++) {
    screen.write(weekLetters[(i + weekDay) % 7]);

/* Draws two horizantal graph bars, representing the total exercise time across the past two weeks */
void drawWeekCompare(int w1, int w2) {

  //See how much we've improved since last week
  int percentChange = ((w2 - w1) * 100) / w1;

  screen.setCursor(0, 0);
  screen.print("Last week");
  drawHBar(w1 / 300, 1);

  screen.setCursor(0, 2);
  screen.print("This week (");

  //Add a plus if we've gone up (negative added automatically)
  if (percentChange >= 0) {


  drawHBar(w2 / 300, 3);

//Writes each of the horizantal progress bar chars into the LCD
void initBarChars() {
  //Start with an empty glyph for the bargraph char
  byte hBar[] = {0, 0, 0, 0, 0, 0, 0, 0};

  //Loop through each bargraph level
  for (int i = 0; i < 5; i++) {
    //Loop through each row of the character
    for (int row = 0; row < 8; row++) {
      //Add in a vertical bar (via bitwise OR)
      hBar[row] = hBar[i] | (0B10000 >> i);

    screen.createChar(i, hBar);

/* Draws a horizantal graphbar x pixels wide on the given LCD row */
void drawHBar(int x, int row) {
  screen.setCursor(0, row);

  int offset = 0;
  while (x - offset > 0) {
    int blockChar = x - offset;
    blockChar = constrain(blockChar, 0, 5);

    screen.write((blockChar == 0) ? 32 : blockChar - 1);

    offset += 5;

//Writes each of the bargraph level chars into the LCD
void initGraphChars() {
  //Start with an empty glyph for the bargraph char
  byte bar[] = {0, 0, 0, 0, 0, 0, 0, 0};

  //Loop through each bargraph level
  for (int i = 0; i < 8; i++) {
    //Set the lowest line to be all ones
    bar[7 - i] = 0B11111;
    screen.createChar(i, bar);

/* Draws a vertical bar 'level' pixels high capped at a given height in char blocks
   at the given position (measured from the top of the bar space) */
void drawGraphBar(int level, int row, int col, int height) {

  for (int y = 0; y < height; y++) {
    int offset = y * 8;
    screen.setCursor(col, row + (height - 1) - y);

    int blockHeight = level - offset;

    //Limit the block height to 0-8
    blockHeight = constrain(blockHeight, 0, 8);

    screen.write((blockHeight == 0) ? 32 : blockHeight - 1);

/* Take the sum of exercise time across an interval in our records */
int getTotalTime(int start, int finish) {
  int out = 0;

  for (int i = start; i < finish; i++) {
    out += pastTimes[i];

  return out;

/* Starts and stops the exercise timer */
void timer()
  long unixTime =;

  if (!timerOn) {
    start = unixTime;
  else {
    timeSeconds = unixTime - start;

    pastTimes[27] += timeSeconds;
    //Set the screen up to show the bargraph
    screenState = 2;
    updateTime = millis() - 3000;

  timerOn = !timerOn;

/* Moves all recorded times over one day in memory (so day 27 is always the most recent) */
void shiftTimes()
  for (int j = 0; j < 27; j++)
    pastTimes[j] = pastTimes[j + 1];

  pastTimes[27] = 0;

/* Updates that need to be performed once each day */
void newDaySetup()
  timeSeconds = 0;

/* Decide if a day has passed and we need to update things */
boolean isNewDay()
  DateTime now =;
  if (now.hour() == 0 && now.minute() == 0 and now.second() == 0)

    return true;

    isMidnight = false;
  return false;

/* Plays the given sound effect */
void playSound(int noteArray[])
  int noteLength = noteArray[0];
  for (int thisNote = 0; thisNote < noteLength; thisNote++) {
    int noteDuration = noteArray[1 + thisNote];
    int notePitch = noteArray[2 * noteLength - noteLength + 1 + thisNote];

    int noteTime = 300 / noteDuration;
    tone(SPEAKER_PIN, notePitch, noteTime);

    int pauseBetweenNotes = noteTime * 1.30;


/* Update the LEDs to reflect which days we have and haven't logged times */
void updateLEDs() {
  for(int i = 0; i < LED_CNT; i++) {
    digitalWrite(LED_BASE_PIN, pastTimes[27 - i] > GOAL_TIME);


 * pitches.h
 * Public Constants

#define NOTE_B0  31
#define NOTE_C1  33
#define NOTE_CS1 35
#define NOTE_D1  37
#define NOTE_DS1 39
#define NOTE_E1  41
#define NOTE_F1  44
#define NOTE_FS1 46
#define NOTE_G1  49
#define NOTE_GS1 52
#define NOTE_A1  55
#define NOTE_AS1 58
#define NOTE_B1  62
#define NOTE_C2  65
#define NOTE_CS2 69
#define NOTE_D2  73
#define NOTE_DS2 78
#define NOTE_E2  82
#define NOTE_F2  87
#define NOTE_FS2 93
#define NOTE_G2  98
#define NOTE_GS2 104
#define NOTE_A2  110
#define NOTE_AS2 117
#define NOTE_B2  123
#define NOTE_C3  131
#define NOTE_CS3 139
#define NOTE_D3  147
#define NOTE_DS3 156
#define NOTE_E3  165
#define NOTE_F3  175
#define NOTE_FS3 185
#define NOTE_G3  196
#define NOTE_GS3 208
#define NOTE_A3  220
#define NOTE_AS3 233
#define NOTE_B3  247
#define NOTE_C4  262
#define NOTE_CS4 277
#define NOTE_D4  294
#define NOTE_DS4 311
#define NOTE_E4  330
#define NOTE_F4  349
#define NOTE_FS4 370
#define NOTE_G4  392
#define NOTE_GS4 415
#define NOTE_A4  440
#define NOTE_AS4 466
#define NOTE_B4  494
#define NOTE_C5  523
#define NOTE_CS5 554
#define NOTE_D5  587
#define NOTE_DS5 622
#define NOTE_E5  659
#define NOTE_F5  698
#define NOTE_FS5 740
#define NOTE_G5  784
#define NOTE_GS5 831
#define NOTE_A5  880
#define NOTE_AS5 932
#define NOTE_B5  988
#define NOTE_C6  1047
#define NOTE_CS6 1109
#define NOTE_D6  1175
#define NOTE_DS6 1245
#define NOTE_E6  1319
#define NOTE_F6  1397
#define NOTE_FS6 1480
#define NOTE_G6  1568
#define NOTE_GS6 1661
#define NOTE_A6  1760
#define NOTE_AS6 1865
#define NOTE_B6  1976
#define NOTE_C7  2093
#define NOTE_CS7 2217
#define NOTE_D7  2349
#define NOTE_DS7 2489
#define NOTE_E7  2637
#define NOTE_F7  2794
#define NOTE_FS7 2960
#define NOTE_G7  3136
#define NOTE_GS7 3322
#define NOTE_A7  3520
#define NOTE_AS7 3729
#define NOTE_B7  3951
#define NOTE_C8  4186
#define NOTE_CS8 4435
#define NOTE_D8  4699
#define NOTE_DS8 4978