Overview

The Programmable Workout Timer is a device that times a user inputed workout and alerts the user when rest and reps should start.

The user inputs workout information before the timing process begins.

During the workout, the device uses a countdown and lights to inform the user when to begin and end exercises.

A speaker alerts the user at the end of a rep or rest period.

The back of the device can be opened to change the battery and perform maintenance.

An example transition from rep in progress to rest.

Process Images and Review

The original state transition planning for the timer.

One of the biggest challenge of this project was coordinating the input and outputs of the device.  This task was particularly difficult, because the same component could interact with different variables at different times in the program.  For example, the keypad is used to enter the information for multiple variables, and the display screen will display a variety of different texts at different times.  Clearly, it was necessary to create some sort of organizational model to better coordinate these processes.

In order to achieve this task, I implemented a multi state system, where the functions of the device were dependent on the state.  Each mode would have a different input, output, and functionality.  The state choices were very natural, as each state could align with a different screen that was displayed to the user.  This decision ultimately made the programming process much less chaotic and more organized.

The code for the “Set Entry” state,  where the user enters the number of sets in their workout.

Another important design choice was made later in the process after testing my original prototype.  I found it frustrating that when I typed a wrong number, I could not delete it like on a normal phone or laptop keypad.  Therefore, I decided to turn the ‘B’ button into a backspace functionality using string manipulation.  While adding the ability to backspace, I also restricted the user from inputing non number values or blank inputs.  These simple changes to the keypad made the project much more user friendly and resistant to errors.

The original electronic prototype.  When testing this system, I realized not being able to delete inputs was frustrating.

Discussion

This project challenged me to do many tasks that I had never done before, and as a result I made many important realizations about larger scale electronics projects.  The largest of these realizations is the importance of planning the physical enclosure of a circuit.  I struggled to fit my circuit inside a reasonably portable box, because I assumed that if all components could fit then the circuit could fit.  However, I did not account for the fact that wires took up significant space and could limit the rotation of the components.  This was a large road block in developing my final product, as I had to redo the wiring for some components.  If I had planned better from the start, I could have saved a lot of time.  Another takeaway from the project is that I really enjoy the coding aspect of circuits involving Arduino’s.  This project is the first time I’ve written Arduino code of this length and magnitude, and naturally I had to learn along the way to overcome obstacles.   Learning more about the Arduino language (and C based languages in general) was a really fun and rewarding part of the process for me.  In particular, learning to manipulate strings and chars was a really useful task that I would like to use again for another project.

Overall, I am very happy with the outcome of this project.  It fulfills the task that I created it for, and I have used it multiple times when working out.  The casing has been durable enough to withstand travel in my backpack, and it is also portable enough to fit easily.  As far as the electronics go, the device effectively tracks and communicates the timing that I need it to do, and it has been a big improvement on using a conventional stopwatch in my workouts.

However, after using the device I’ve realized that there are a few additions that could be beneficial.  One drawback of the current system is that it assumes all rests in a set are the same length, but occasionally I will do a sprint workout where the rests in-between reps are different.  An additional mode which can program more complicated workout structures would be a useful.  Furthermore, I occasionally want to extend a rest period or cut it short, so an “add 30 seconds” or “start now” feature may be a good addition.  Lastly, when the track is wet I worry about damaging the electronics, so waterproofing the case could be beneficial.

When looking at written critiques, it is apparent there are other possible improvements  to consider.  One classmate commented,  “A good modification would be having a selection of preset workouts.”  I think this is a very interesting idea, but I do not think I will implement it.  Almost every week I slightly modify the workouts I am doing.  Therefore,  while I often do similar workouts, I rarely do identical workouts, so this change would not be very beneficial for me personally.  However, if I were to make this product for mass distribution, adding a preset workout feature could be more useful.  Another classmate suggested “making the indicators or sounds louder or bigger,” and I agree that this should be changed in the next iteration.  In the prototype, the speaker was much louder, but when using the Arduino Nano and battery power it became  fainter.  Using an amplifier or a louder speaker could certainly improve the quality of the project.

 

Technical Information

Schematic:

Code:

/*
 * Programmable Workout Timer
 * Justin Kiefel (jkiefel)
 * 
 * Description: This project allows a user to input workout data using a 
 * 4x3 keypad and tracks the time of the workout.  It gives alerts through 
 * a speaker, LED's, and an LCD display when a rep or rest is beginning, ending, 
 * or almost finished.  This project was designed specifically for sprint workouts,
 * but its functionality can be transferred to many other workouts.
 * 
 * Credit: 
 * Keypad and LCD Setup Code From - http://www.circuitbasics.com/how-to-set-up-a-keypad-on-an-arduino/
 * Pitch Code From - http://www.arduino.cc/en/Tutorial/Tone
 * Libraries Used - LiquidCrystal_I2C, Keypad
 * 
 * Summary: 
 * 
 * Inputs: 
 *  Arduino pin | input
 *  2-9           keypad
 *  A4            LCD SDA
 *  A5            LCD SCL
 *  
 * Outputs:
 *  Arduino pin | output
 *  10            speaker
 *  11            green LED
 *  12            blue LED
 *  13            red LED
 */

// keypad/lcd setup code from
// 

#include "pitches.h" 
#include <Wire.h>
#include<LiquidCrystal_I2C.h>
#include<Keypad.h>

// keypad setup
const byte ROWS = 4;
const byte COLS = 4;

const byte rowPins[ROWS] = {9, 8, 7, 6};
const byte colPins[COLS] = {5, 4, 3, 2};

const char hexaKeys[ROWS][COLS] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'}
};

Keypad userKeypad = Keypad(makeKeymap(hexaKeys), rowPins, colPins, ROWS, COLS);

// lcd setup
LiquidCrystal_I2C lcd(0x27, 16, 2);  

// pin setup
const int SPEAKER_PIN = 10;
const int REP_PIN = 11;
const int SOON_PIN = 12;
const int REST_PIN = 13;

// speaker note data
const int repNoteDuration[] = {8,8};
const int repNoteMelody[]{NOTE_B3, NOTE_B5};

const int restNoteDuration[] = {8, 8};
const int restNoteMelody[]{NOTE_B5, NOTE_B3};

const int soonDuration[]{4};
const int soonMelody[]{NOTE_B3};

// initializing variables 
int state = 0; // current device state

String sets = ""; // user input number of sets
String reps = ""; // user input number of reps
String tpr = ""; // user input time per rep
String rpr = ""; // user input rest per rep
String rps = ""; // user input rest per set

int setsInt = 0; // number of sets
int repsInt = 0; // number of reps
int tprInt = 0;  // time per rep
int rprInt = 0;  // rest per rep
int rpsInt = 0;  // rest per set

int copyOfReps = 0; // used to remember number of reps
int startingCountdown = 4; // length of the starting countdown

int lcdTimer = 0; // the current integer to be displayed on the LCD screen 
int prevLcdTimer = 0; // the previous integer on the LCD screen

unsigned long timeMarker = 0; // used to track time at the start of a state

bool sound1Done = 0; // indicates the completion of the 'starting state' outputs
bool sound2Done = 0; // indicates the completion of the 'almost done' outputs

void setup() {
  lcd.backlight();
  lcd.init();
  lcd.home();
  
  pinMode(SPEAKER_PIN, OUTPUT);
  pinMode(REP_PIN, OUTPUT);
  pinMode(SOON_PIN, OUTPUT);
  pinMode(REST_PIN, OUTPUT);
}

void loop() {  
  char userInput = userKeypad.getKey(); // gets user input

  if (state == 0){ // opening screen 
    lcd.home();
    lcd.print("press * to start");
    if (userInput == '*'){
      state = 1;
      lcd.clear();
    }
  }

  else if (state == 1){ // set entry screen
    lcd.home();
    lcd.print("enter # of sets");
    lcd.setCursor(0,1); 
    
    if (isDigit(userInput)){ // typing functionality
      sets = sets+String(userInput);
    }
    else if (userInput == 'B'){ // backspace functionality
      sets = sets.substring(0,(sets.length()-1));
      lcd.clear();
      Serial.println(sets);
    }
    else if (userInput == '*' and sets != ""){ // next state functionality
      state = 2;
      lcd.clear();
    }
    lcd.print(sets);
  }

  else if (state == 2){ // rep entry screen
    lcd.home();
    lcd.print("enter # of reps");
    lcd.setCursor(0,1);
    
     if (isDigit(userInput)){ // typing functionality
      reps = reps+String(userInput);
    }
    else if (userInput == 'B'){ // backspace functionality
      reps = reps.substring(0,(reps.length()-1));
      lcd.clear();
    }
    else if (userInput == '*' and reps != ""){ // next state functionality
      state = 3;
      lcd.clear();
    }
    lcd.print(reps);
  }

  else if (state == 3){ // time per rep entry screen
    lcd.home();
    lcd.print("time per rep (s)?");
    lcd.setCursor(0,1);
    
     if (isDigit(userInput)){ // typing functionality
      tpr = tpr+String(userInput);
    }
    else if (userInput == 'B'){ // backspace functionality
      tpr = tpr.substring(0,(tpr.length()-1));
      lcd.clear();
    }
    else if (userInput == '*' and tpr != ""){ // next state functionality
      state = 4;
      lcd.clear();
    }
    lcd.print(tpr);
  }

  else if (state == 4){ // rest per rep entry screen
    lcd.home();
    lcd.print("rest per rep (s)");
    lcd.setCursor(0,1);
    
     if (isDigit(userInput)){ // typing functionality
      rpr = rpr+String(userInput);
    }
    else if (userInput == 'B'){ // backspace functionality
      rpr = rpr.substring(0,(rpr.length()-1));
      lcd.clear();
    }
    else if (userInput == '*' and rpr != ""){ // next state functionality
      state = 5;
      lcd.clear();
    }
    lcd.print(rpr);
  }

  else if (state == 5){ // rest per rep entry screen
    lcd.home();
    lcd.print("rest per set (s)");
    lcd.setCursor(0,1);
    
    if (isDigit(userInput)){ // typing functionality
      rps = rps+String(userInput);
    }
    else if (userInput == 'B'){ // backspace functionality
      rps = rps.substring(0,(rps.length()-1));
      lcd.clear();
    }
    else if (userInput == '*' and rps != ""){ //  next state functionality
      state = 6;
      lcd.clear();
    }
    lcd.print(rps);
  }

  else if (state == 6){ // countdown to start
    startingCountdown = startingCountdown - 1;
    
    lcd.home(); 
    lcd.print("starting in");
    lcd.setCursor(0,1);

    int noteDuration = 1000 / soonDuration[0];
    tone(SPEAKER_PIN, soonMelody[0], noteDuration);
    
    setsInt = sets.toInt() - 1; // to account for first set
    repsInt = reps.toInt() - 1; // to account for first rep 
    tprInt = tpr.toInt();
    rprInt = rpr.toInt(); 
    rpsInt = rps.toInt();
    copyOfReps = repsInt;

    delay(1000);
    lcd.print(startingCountdown);
    
    if (startingCountdown == 0){
      state = 7;
      startingCountdown = 4;
      lcd.clear();
    }
  }

  else if (state == 7){ // rep in progress
    if (not sound1Done){ // rep start light and sound
      digitalWrite(REP_PIN, HIGH);
      timeMarker = millis();
      sound1Done = 1;
      for (int i = 0 ; i < 2 ; i++){
        int noteDuration = 1000 / repNoteDuration[i];
        tone(SPEAKER_PIN, repNoteMelody[i], noteDuration);
        int pause = noteDuration * 1.25;
        delay(pause);
        noTone(10);
      }
    }
    // updates countdown
    lcdTimer = (tprInt - (millis() - timeMarker)/1000);
    
    if (prevLcdTimer != lcdTimer){
      lcd.clear();
      prevLcdTimer = lcdTimer;
    }
    
    lcd.home();
    lcd.print("rep in progress");
    lcd.setCursor(0,1);
    lcd.print(lcdTimer);

    if (((millis() - timeMarker)/1000 > (tprInt*.75)) and (not sound2Done)) { // almost done warning
      sound2Done = 1;
      digitalWrite(SOON_PIN, HIGH);
      int noteDuration = 1000 / soonDuration[0];
      tone(SPEAKER_PIN, soonMelody[0], noteDuration);
    }

    if ((millis() - timeMarker)/1000 > tprInt){ // next state functionality
      digitalWrite(REP_PIN, LOW);
      digitalWrite(SOON_PIN, LOW);
      sound1Done = 0;
      sound2Done = 0;
      lcd.clear();
      
      if (repsInt > 0){ // if more reps in set, then go to rep rest
        state = 8;
      }
      else if (setsInt > 0){
        repsInt = copyOfReps;
        state = 9;
      }
      else{
        state = 0;
        sets = "";
        reps = "";
        tpr = "";
        rpr = "";
        rps = "";
        setsInt = 0;
        repsInt = 0;
        tprInt = 0;
        rprInt = 0;
        rpsInt = 0;
        copyOfReps = 0;
      }
    }
  }

  else if (state == 8){ // rest between reps
    if(not sound1Done){ // rest start light and sound
      timeMarker = millis();
      digitalWrite(REST_PIN, HIGH);
      sound1Done = 1;
      for (int i = 0 ; i < 2 ; i++){
        Serial.print(i);
        int noteDuration = 1000 / restNoteDuration[i];
        tone(SPEAKER_PIN, restNoteMelody[i], noteDuration);
        int pause = noteDuration * 1.25;
        delay(pause);
        noTone(10);
      }
    }
    // updates countdown
    lcdTimer = (rprInt - (millis() - timeMarker)/1000);
    
    if (prevLcdTimer != lcdTimer){ 
      lcd.clear();
      prevLcdTimer = lcdTimer;
    }
    
    lcd.home();
    lcd.print("rest between reps");
    lcd.setCursor(0,1);
    lcd.print(lcdTimer);

    if (((millis() - timeMarker)/1000 > (rprInt*.75)) and (not sound2Done)) { // almost done warning
      sound2Done = 1;
      digitalWrite(SOON_PIN, HIGH);
      int noteDuration = 1000 / soonDuration[0];
      tone(SPEAKER_PIN, soonMelody[0], noteDuration);
    }
    
   if ((millis() - timeMarker)/1000 > rprInt){ // next state functionality
      digitalWrite(REST_PIN, LOW);
      digitalWrite(SOON_PIN, LOW);
      sound1Done = 0;
      sound2Done = 0;
      lcd.clear();
      repsInt = repsInt - 1;
      state = 7;
   }
  } 

  else if (state == 9){ // rest between sets
    if(not sound1Done){ // rest start light and sound
      timeMarker = millis();
      digitalWrite(REST_PIN, HIGH);
      sound1Done = 1;
      for (int i = 0 ; i < 2 ; i++){
        Serial.print(i);
        int noteDuration = 1000 / restNoteDuration[i];
        tone(SPEAKER_PIN, restNoteMelody[i], noteDuration);
        int pause = noteDuration * 1.25;
        delay(pause);
        noTone(10);
      }
    }
    // updates countdown
    lcdTimer = (rpsInt - (millis() - timeMarker)/1000);
    
    if (prevLcdTimer != lcdTimer){
      lcd.clear();
      prevLcdTimer = lcdTimer;
    }

    lcd.home();
    lcd.print("rest between sets");
    lcd.setCursor(0,1);
    lcd.print(lcdTimer);

    if (((millis() - timeMarker)/1000 > (rpsInt*.75)) and (not sound2Done)) { // almost done warning
      sound2Done = 1;
      digitalWrite(SOON_PIN, HIGH);
      int noteDuration = 1000 / soonDuration[0];
      tone(SPEAKER_PIN, soonMelody[0], noteDuration);
    }
    
   if ((millis() - timeMarker)/1000 > rpsInt){ // next state functionality
      digitalWrite(REST_PIN, LOW);
      digitalWrite(SOON_PIN, LOW);
      sound1Done = 0;
      sound2Done = 0;
      lcd.clear();
      setsInt = setsInt - 1;
      state = 7;
    }  
  }
}