Overview
The Programmable Workout Timer is a device that times a user inputed workout and alerts the user when rest and reps should start.
An example transition from rep in progress to rest.
Process Images and Review
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.
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.
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; } } }
Comments are closed.