Top view of the finalized push-up machine without power on
The Push-up Pal is a device that motivates me to "do my push-ups." It accomplishes this by gamifying the process through lights, sounds, and motivational messages, as well as by holding me accountable throughout each individual rep by detecting my weight.
The menu includes three modes: a standard push-up, a push-up with elevated feet, and a one-armed push-up that only requires force to be exerted on one hand plate.
Pictured above is a close-up of the LCD screen at the end of each workout. The LCD changes continuously throughout the experience to direct and engage with the user.
By incorporating foam, the wires could be embedded within the device due. As shown above, the sides were sealed with tape for a cleaner finish.
Above is a photo of the device plugged in. Although the LCD displays are difficult to read, the effects of the lights can be seen.
Shown above is the device being used, as well as it placed next to myself for scaling purposes.
In this video, I walk through the entire process of using the machine in standard mode. The LCD displays are again not visible, but they are narrated as I go along.
To begin, the device initializes with a motivational message. Then, it prompts the user to select a mode while the lights flash. Once the mode has been determined, the user is prompted on the screen to place their hand(s) on the designated placements. When weight is detected, the lights remain on and the user's current score, high score, mode, and calories burned are displayed; additionally, each time the nose button is pressed, the buzzer emits a light beep to indicate that a rep has been added. Finally, as soon as weight is removed from the mats, the workout is automatically ended, the lights turn off, and the final stats are displayed until the device is turned off.
Firstly, I sketched out the general layout of my device along with planned features to visualize the size and look of my final goal.
I practiced doing push-ups and used markers to determine exact dimensions and placements of the hand-plates and nose button.
The final CAD for the main body of the device is shown above. It was made in SolidWorks.
I created life-sized hand files and laser-cut them. I knew, going in, that my project would involve several rounds of laser cutting, so I wanted to finish the hands early.
For my initial prototype, I got an early version of the electronics working. The general circuit consisted of force-sensing resistors, an LCD, several LEDs and buttons, and a buzzer.
Alongside the main body, I also used Fusion to create a finger-jointed acrylic box to hold the nose button and the electronics.
The next step was to fully lay out all of the components at scale, as well as to label and cut all wires to size. This was a very meticulous process due to the sheer number of components running across the board.
To support my body weight, I decided to embed the wires into a layer of foam that sat underneath the main board. I cut the foam to size and carved channels between the electronics box and the location of each component.
I then CADed up the menu pad and laser-cut it, along with the acrylic box that I modeled earlier.
Each wire was soldered onto one of two protoboards. This step was incredibly time-consuming (and somewhat stressful because there was a non-zero chance that a faulty connection would occur and be incredibly hard to debug); however, it was unavoidable because the wire lengths required exceeded maximum jumper lengths.
The connected/soldered electronics were then placed into their intended channels, and functionality/debugging tests were performed.
Lastly, I placed the board over all of the wiring, taped up the sides and added a bottom layer of paper to make it look clean, and screwed the entire thing together.
Overall, this project was a bit simpler on the computational side, with each button/light aspect being relatively straightforward; however, it was a very demanding process in terms of fabrication. When I initially had the idea, I didn't think through the difficulty of creating a flat product that could withstand a significant amount of weight. When I began to truly lay out my game plan, I realized that creating a clean finish would involve some creativity, hence the foam. Overall, the push-up machine took many, many hours, and much of the work is hidden underneath the top layer; nonetheless, I'm happy with how closely the final device fit my initial vision.
Many of the comments that I received were along a similar vein: people generally liked the silly and whimsical nature of the device, with one person stating that I “really brought out the playful nature” of the concept. This was heartwarming to hear, especially because I was initially a bit unsure of how people would receive such an unserious product. In terms of suggested improvements, one critiquer asked whether the device could be “taken apart and reassembled for transport,” which was a point that I hadn’t considered but wish that I had. If I were to make a second iteration of my prototype, my next goal would likely be compactability. Currently, the device is sitting on my floor, taking up a significant amount of idle space; thus, some sort of creative folding mechanism could improve accessibility for those with small living areas. Other priorities for future iterations include non-slip material for the hand-pads, more securely attached buttons (my holes were slightly too large), and adjustable sizing for different push-up wingspans.
With all that being said, I am satisfied with my final product. My goals were mainly functionality (does it support weight without sustaining damage? Do the features reliably work as intended?) and overall aesthetic, both of which I achieved to a relatively high level. I’m particularly proud of my time management skills throughout the building process. During our short prototyping window, I was juggling several deadlines, so I heavily frontloaded the work. Generally, I’ve found that project timelines (particularly those within groups) are hard to maintain; thus, I’m happy with at how well I stuck to the schedule. I am also pleased with my attention to detail regarding the soldered connections and sleek detail finishes, as well as my creativity in implementing the foam during a period of confusion about how to proceed.
Throughout this process, I also reinforced some of my own knowledge about my abilities and limitations. I enjoy physical tasks far more than computational ones, so software writing was the least intuitive/enjoyable part for me. In terms of soldering, I tended to solder far too many connections before testing for functionality; therefore, if I were to redo this process, I would debug earlier and more frequently.
/*
Push-up Machine Script: interacts with user during push-ups by counting reps, flashing lights, and allowing for switching between modes
Pin Mapping:
Arduino Pin | Role | Description
___________________________________________________________________
A0 input force-sensing resistor (1)
A1 input force-sensing resistor (2)
3 input menu button (1)
4 input menu button (2)
5 input menu button (3)
6 output LED (1-2)
7 output LED (3-4)
8 output LED (5-6)
9 output LED (7-8)
10 output LED (9)
12 input buzzer
13 input nose button
Rongrong Wang; rongronw@andrew.cmu.edu
Note: ChatGPT was used in the writing and debugging of this script
*/
#include <EEPROM.h>
#include <LiquidCrystal_I2C.h>
// define FSR pins
const int FSRPIN_1 = A0;
const int FSRPIN_2 = A1;
// define constants for voltage and resistance calculations
const float VCC = 4.98;
const float R_DIV = 3230.0;
// define buzzer and nose button pins
const int NOSEBUTTONPIN = 13;
const int BUZZERPIN = 12;
// define LED pins
const int LEDPIN_1 = 11;
const int LEDPIN_2 = 10;
const int LEDPIN_3 = 9;
const int LEDPIN_4 = 8;
const int LEDPIN_5 = 7;
const int LEDPIN_6 = 6;
// define menu button pins
const int MENUBUTTONPIN_1 = 5; // standard
const int MENUBUTTONPIN_2 = 4; // elevated
const int MENUBUTTONPIN_3 = 3; // one-armed
// initialize conditions for menu
bool standard = false;
bool elevated = false;
bool oneArm = false;
// define force values
float force_1 = 0;
float force_2 = 0;
// create storage variables
int repCount = 0;
int highScore = 0;
int standardHighScore = 0;
int elevatedHighScore = 0;
int oneArmHighScore = 0;
#define EEPROM_STD_ADDR 0 // address for standard mode high score
#define EEPROM_ELEV_ADDR 4 // address for elevated mode high score
#define EEPROM_ONEARM_ADDR 8 // address for one-arm mode high score
// initialize LCD
LiquidCrystal_I2C screen(0x27, 20, 4);
// track the time of the button presses
unsigned long lastButtonPressTime = 0;
bool buttonsPressedRecently = false;
void setup() {
Serial.begin(9600);
// read saved high scores from EEPROM
EEPROM.get(EEPROM_STD_ADDR, standardHighScore);
EEPROM.get(EEPROM_ELEV_ADDR, elevatedHighScore);
EEPROM.get(EEPROM_ONEARM_ADDR, oneArmHighScore);
// ensure values are reasonable (EEPROM defaults to 255 when empty)
if (standardHighScore > 10000 || standardHighScore < 0) standardHighScore = 0;
if (elevatedHighScore > 10000 || elevatedHighScore < 0) elevatedHighScore = 0;
if (oneArmHighScore > 10000 || oneArmHighScore < 0) oneArmHighScore = 0;
pinMode(FSRPIN_1, INPUT);
pinMode(FSRPIN_2, INPUT);
pinMode(BUZZERPIN, OUTPUT);
pinMode(NOSEBUTTONPIN, INPUT_PULLUP);
pinMode(LEDPIN_1, OUTPUT);
pinMode(LEDPIN_2, OUTPUT);
pinMode(LEDPIN_3, OUTPUT);
pinMode(LEDPIN_4, OUTPUT);
pinMode(LEDPIN_5, OUTPUT);
pinMode(LEDPIN_6, OUTPUT);
screen.init();
screen.backlight();
screen.print("Welcome back.");
screen.setCursor(0, 2);
screen.print("Get after it?");
delay(2500);
screen.clear();
// blink LEDs while selecting a type
screen.print("Select push-up type.");
blinkLEDsUntilSelection();
pinMode(MENUBUTTONPIN_1, INPUT_PULLUP);
pinMode(MENUBUTTONPIN_2, INPUT_PULLUP);
pinMode(MENUBUTTONPIN_3, INPUT_PULLUP);
// wait for user input
while (true) {
if (digitalRead(MENUBUTTONPIN_1) == LOW) {
standard = true;
break;
}
if (digitalRead(MENUBUTTONPIN_2) == LOW) {
elevated = true;
break;
}
if (digitalRead(MENUBUTTONPIN_3) == LOW) {
oneArm = true;
break;
}
}
screen.clear();
if (standard) screen.print("Mode: Standard");
else if (elevated) screen.print("Mode: Elevated");
else if (oneArm) screen.print("Mode: One-Arm");
delay(2000);
screen.clear();
screen.print("Place hands on mats.");
// blink LEDs while waiting for hands to be placed
blinkLEDsUntilHandPlacement();
// turn LEDs ON for the entire workout
turnOnLEDs();
screen.clear();
updateLCD(); // display initial values
}
void loop() {
// check if all three buttons are pressed within 2 seconds to reset high scores
if (digitalRead(MENUBUTTONPIN_1) == LOW && digitalRead(MENUBUTTONPIN_2) == LOW && digitalRead(MENUBUTTONPIN_3) == LOW) {
if (!buttonsPressedRecently) {
lastButtonPressTime = millis(); // Record the time when the buttons are pressed
buttonsPressedRecently = true;
}
}
// if buttons are pressed and 2 seconds have passed, reset high scores
if (buttonsPressedRecently && millis() - lastButtonPressTime < 2000) {
// reset the high scores
standardHighScore = 0;
elevatedHighScore = 0;
oneArmHighScore = 0;
// save the reset high scores to EEPROM
EEPROM.put(EEPROM_STD_ADDR, standardHighScore);
EEPROM.put(EEPROM_ELEV_ADDR, elevatedHighScore);
EEPROM.put(EEPROM_ONEARM_ADDR, oneArmHighScore);
// display a reset message on the LCD
screen.clear();
screen.setCursor(0, 0);
screen.print("High scores reset!");
delay(2000); // Show the message for 2 seconds
// reset the flag for button presses
buttonsPressedRecently = false;
}
else if (millis() - lastButtonPressTime > 2000) {
// ff it's been more than 2 seconds, reset the button press detection
buttonsPressedRecently = false;
}
determineState();
detectForce();
if ((standard || elevated) && force_1 >= 500 && force_2 >= 500) {
updateLCD();
} else if (oneArm && (force_1 >= 500 || force_2 >= 500)) {
updateLCD();
}
static unsigned long lastButtonPress = 0;
const unsigned long debounceDelay = 300; // 300ms debounce delay
if (digitalRead(NOSEBUTTONPIN) == LOW && millis() - lastButtonPress > debounceDelay) {
repCount += 1;
tone(BUZZERPIN, 1000, 100);
lastButtonPress = millis();
updateLCD(); // force update after rep increment
}
// update high scores and store them in EEPROM if they increase
if (repCount > highScore) {
highScore = repCount;
if (standard && highScore > standardHighScore) {
standardHighScore = highScore;
EEPROM.put(EEPROM_STD_ADDR, standardHighScore);
} else if (elevated && highScore > elevatedHighScore) {
elevatedHighScore = highScore;
EEPROM.put(EEPROM_ELEV_ADDR, elevatedHighScore);
} else if (oneArm && highScore > oneArmHighScore) {
oneArmHighScore = highScore;
EEPROM.put(EEPROM_ONEARM_ADDR, oneArmHighScore);
}
}
if ((force_1 < 500 && force_2 < 500) && repCount >= 1) {
endWorkout();
}
}
void determineState() {
if (standard) {
highScore = standardHighScore;
} else if (elevated) {
highScore = elevatedHighScore;
} else if (oneArm) {
highScore = oneArmHighScore;
}
}
void detectForce() {
int fsrRead_1 = analogRead(FSRPIN_1);
int fsrRead_2 = analogRead(FSRPIN_2);
Serial.print("FSR Raw 1: ");
Serial.print(fsrRead_1);
Serial.print(" | FSR Raw 2: ");
Serial.println(fsrRead_2);
// convert FSR readings into voltage
float fsrVoltage_1 = fsrRead_1 * (VCC / 1023.0);
float fsrVoltage_2 = fsrRead_2 * (VCC / 1023.0);
Serial.print("FSR Voltage 1: ");
Serial.print(fsrVoltage_1, 3);
Serial.print(" V | FSR Voltage 2: ");
Serial.println(fsrVoltage_2, 3);
// compute resistance only if the voltage is nonzero
float fsrResistance_1 = (fsrVoltage_1 > 0) ? ((VCC - fsrVoltage_1) * R_DIV / fsrVoltage_1) : 1000000;
float fsrResistance_2 = (fsrVoltage_2 > 0) ? ((VCC - fsrVoltage_2) * R_DIV / fsrVoltage_2) : 1000000;
Serial.print("FSR Resistance 1: ");
Serial.print(fsrResistance_1);
Serial.print(" Ω | FSR Resistance 2: ");
Serial.println(fsrResistance_2);
// convert resistance to force (Approximate equation for FSR)
if (fsrResistance_1 < 10000) { // Prevents very high resistance values from affecting force
force_1 = (1.0 / fsrResistance_1) * 1000000;
} else {
force_1 = 0;
}
if (fsrResistance_2 < 10000) {
force_2 = (1.0 / fsrResistance_2) * 1000000;
} else {
force_2 = 0;
}
Serial.print("Force 1: ");
Serial.print(force_1);
Serial.print(" g | Force 2: ");
Serial.println(force_2);
}
void updateLCD() {
float calories = repCount * 0.29;
screen.setCursor(0, 0);
screen.print("Mode: " + String(standard ? "Standard" : (elevated ? "Elevated" : "One-Arm")));
screen.setCursor(0, 1);
screen.print("Count: " + String(repCount) + " ");
screen.setCursor(0, 2);
screen.print("High Score: " + String(highScore) + " ");
screen.setCursor(0, 3);
screen.print("Calories: " + String(calories, 1) + " kcal ");
}
void endWorkout() {
screen.clear();
screen.setCursor(0, 0);
screen.print("Workout Ended.");
screen.setCursor(0, 1);
screen.print("Final Count: " + String(repCount));
screen.setCursor(0, 2);
screen.print("High Score: " + String(highScore));
float calories = repCount * 0.29;
screen.setCursor(0, 3);
screen.print("Calories: " + String(calories, 1) + " kcal");
// turn off LEDs at the end of the workout
turnOffLEDs();
while (true)
; // freeze execution
}
// blink LEDs every 0.5 seconds while waiting for push-up type selection
void blinkLEDsUntilSelection() {
while (true) {
turnOnLEDs();
delay(500);
turnOffLEDs();
delay(500);
if (digitalRead(MENUBUTTONPIN_1) == LOW || digitalRead(MENUBUTTONPIN_2) == LOW || digitalRead(MENUBUTTONPIN_3) == LOW) {
break;
}
}
}
// blink LEDs every 0.5 seconds while waiting for hands to be placed
void blinkLEDsUntilHandPlacement() {
while (true) {
turnOnLEDs();
delay(500);
turnOffLEDs();
delay(500);
if (analogRead(FSRPIN_1) > 500 || analogRead(FSRPIN_2) > 500) {
break;
}
}
}
// functions to turn LEDs ON/OFF
void turnOnLEDs() {
digitalWrite(LEDPIN_1, HIGH);
digitalWrite(LEDPIN_2, HIGH);
digitalWrite(LEDPIN_3, HIGH);
digitalWrite(LEDPIN_4, HIGH);
digitalWrite(LEDPIN_5, HIGH);
digitalWrite(LEDPIN_6, HIGH);
}
void turnOffLEDs() {
digitalWrite(LEDPIN_1, LOW);
digitalWrite(LEDPIN_2, LOW);
digitalWrite(LEDPIN_3, LOW);
digitalWrite(LEDPIN_4, LOW);
digitalWrite(LEDPIN_5, LOW);
digitalWrite(LEDPIN_6, LOW);
}