With this project, we set out to design and build a device to help an older person in the Pittsburgh community. After an initial meeting with Gary (the older person who we partnered with), we decided to create a way to help people who are recovering from a stroke to quantitatively measure the resulting strength differences between both sides of their body. We spent several weeks creating a prototype, and received lots of valuable feedback that ultimately led us to this final product.

 

What We Built

We created a device that enables users to perform several different physical exercises such as bicep curls, lateral raises, and many other resistance band exercises in a home gym setting. They are then able to access information on their performance including max weight, the number of repetitions, and how their left and right sides compare to each other. To accomplish this, we decided on anchoring a sensor connected to a resistance band to the base of a door since we saw this as a convenient location that everyone would have in their homes. Adjacent to the sensor is an interface that users can use to see the results of their exercises.

The device has a main display that welcomes the user and then prompts them to begin an exercise on either their left or right side. Once a side is selected, there is a brief countdown to allow the user to get into position and begin the exercise. After the exercise is completed, the user can either perform another exercise on the other side or view their results at any time.

Device Functionality
Project Photos

There is an adjustable metal bracket that allows the device to be anchored underneath doors of varying widths. Foam/rubber pads protect the door and prevent the device from sliding horizontally along the bottom of the door.

Connected directly to the metal bracket is a load cell that measures the force pulling on it from a resistance band. The load cell is connected in such a way that it can pivot in a 90 degree arc allowing for many more exercises to be done.

On the bottom side of the device is a removable panel that gives users access to the battery holder in order to change batteries once they are worn out. Additionally, to save battery, there is an on/off switch on the side of the device so that the Arduino only draws power when the device is in use.

The device is anchored by ensuring that the adjustable bracket is set to the correct width and then sliding it underneath a door.

A user performing a bicep curl.

Users can change the resistance by stepping further/closer to the door and then performing their exercise.

By pressing buttons on the device, users can change sides, switch exercises, and view the results of their workout.

The following is a brief narrative sketch to help describe how our device fits in the context of everyday life:

Gary suffered a stroke and has been working hard in his home gym to correct the resulting muscle imbalance. While Gary has a trainer to help him do this, neither Gary nor his trainer have a way to quantitatively measure fine differences in strength on either side of Gary’s body.

At the beginning of each week, Gary performs several exercises such as bicep curls, lateral raises, and leg lifts using the device, and notes which side of his body is stronger and by how much. He is then able to work with his trainer to structure his week’s workout routine with the goal of balancing his strength accordingly. He is able to use the device at any point (similar to how one might use a scale to check their weight) in order to see his progress and have hard numbers to work with in improving his physical strength and becoming more balanced.

 

How We Got Here

The load cell sensor we were worked with

One of the first decisions we made as a team was the choice of sensors. Due to the form factor and flexibility we needed in our solution, a load cell seemed like the best bet, but we also initially considered using pressure sensors. The choice of load cell was the decision we look back on and wish we could have changed. We selected a 40kg bearing load cell from Amazon, which also came with an integrated board that amplified signals from the sensor and worked perfectly with an Arduino. Not only was the hardware supposed to integrate perfectly with our Arduino, but the product included a well written tutorial on how to capture signals from the load cell. In being content with the tight integration with our Arduino, however, we failed to consider just how flimsy and difficult the load cell would be to work with.

Resoldering the broken wires on the load cell

The load cell was designed to support 40kg, but the wires that detected changes in force were extremely thin and connected to the sensor with a fine plastic coating. This coating barely held the wires in place and we ended up tearing one of the wires within a few days of development. Attempts to solder it back were fruitless because there was limited wire to metal contact and the plastic coating had burned. In the end, a new, second sensor that we took extreme precaution with also broke, leading us to conclude that a more durable load cell would have been better from the get go. In fact, we have ordered a new one to be used in the final product we give to Gary.

Full electronic prototype, with working load cell, buttons, and screen interface

The lesson we learned from this was to first analyze how a physical component would perform in the real world as opposed to determining its simplicity for use with hardware or software. The sensor not only created trouble when assembling and testing our device, but also in the software development phase because we didn’t know if the problem was with the physical load cell or with the software. Before we knew that it was the weak wiring that was causing the problem, this was highly frustrating.

One of the challenging aspects of our project was repetition counting and weight measurement. After discussing with our professor and understanding the processing limits of the Arduino, we decided to implement a windowing approach. Our code kept a few hundred milliseconds of data and averages in buffers and calculated local maxima based on the slope of the data.

Signals captured from exercise repetitions, when the sensor was working

When we tested this on actual measurements from the device, it worked extremely well. This was a breakthrough moment for us and helped refocus our efforts on the physical and reliability aspects of the device. In general, the software aspect of our project was not very time consuming and we were done with that part ahead of schedule.

Testing the load cell while it was not working, not knowing what the problem was (feat. helpful non-class member)

Frustration with device – it was not communicating with the computer

The physical construction of the device was something we discussed at length. At first, we were debating whether to use a device mounted to the door (as in our final product) or to use something in line with a resistance band, to measure the force exerted by pulling on both sides of the band. Then, we needed to decide how best to house the load cell sensor as well as the screen and buttons interface – we considered having two parts to the project, so that the screen and buttons could perhaps be a separate handheld component. However, we decided against this as it would require two Arduinos and thus two sources of power, making the whole setup less clean. We eventually settled on a door-mounted, single-box design.

Sketches of ideas for housing the device and load cell

Assembling the housing using plastic solvent

Fitting the electronics into the housing. Finding an intuitive way to organize the Arduino, sensors, battery holder, etc. proved more challenging than we initially thought.

We didn’t follow our schedule too well and underestimated how long it would take to physically wire up and build the device, but were able to manage the rest of our time fairly effectively. This was possible because we delegated certain aspects of the project to each group member according to their strengths.

 

Conclusions and Lessons Learned

Several people stated that they “would like to see a smaller device” more in line with the resistance band, as something smaller might be more convenient to use on a regular basis. We had already recognized this as a way to continue improving our design, and were relatively unsurprised that the size of the device was brought up by multiple people. However, a comment stated that it was unfortunate that the user had to bend down to press the buttons, which is a good point. We thought about ways we could have fixed this, but couldn’t find any solutions that didn’t involve having another, separate part of the device,

Many of our reviews mention that the device “feels fragile and makes the user afraid to apply full force”, although the force is not applied directly to the box. During the final crit, we found ourselves explaining this to people repeatedly, which is a sign that we should have made our device visibly appear more sturdy. We believe this perception of our device could be due to the transparency of the acrylic used for the housing, although this has no impact on its actual strength. We did not realize how much this would affect people’s perception of the device.

Our device intrigued the Osher students who observed it, and throughout the crit we were asked if the device could be applied to multiple different exercises. We showed them different ways to hold the resistance band to work different muscles, but a prevailing idea seemed to be to provide multiple attachments for the device (not just the resistance band). Our device is already capable of doing this, since the attachment would just need to be able to connect to a carabiner, but this feedback reinforced the idea of preserving the adaptability of our device to multiple exercises and situations. For example, having our device be able to handle “different door placements”, different exercises, and different amounts of force.

Working with Gary was a pleasure – he had a clear idea of what he wanted from the very beginning, which surprised us but was very helpful in getting us started. He was always available for feedback, and seemed very happy with the direction we were going in. However, we were working somewhat blindly, as he didn’t tell us what specific exercises he did and we just worked off our best guess of what he needed. We happened to make the right choices, but were I to do this again I would make sure to ask for all the information we need from the start.

Overall, we had a relatively even distribution of work over time, but fell off schedule and spent a large portion of a day putting everything together. Doing this again, we would attempt to stay on track and finish different parts of the project by the scheduled times. This way, we would be able to achieve our longer-term goals, such as making our design more compact by using a smaller Arduino or microcontroller.

It was a very rewarding experience to be able to make something suited to someone we have been talking to for over a month, and have it suit his specific needs, but also be applicable to many other situations. Gary actually wants to take this project forward and develop it into a more marketable product, which is unexpected, but it is very exciting that Gary thinks that our product has potential in the market.

Technical Details

Code

/*
 * Strength Balance
 * 
 * Description: We created a device that enables users to perform several different physical exercises 
 * such as bicep curls, lateral raises, and many other resistance band exercises in a home gym setting. 
 * They are then able to access information on their performance including max weight, the number of 
 * repetitions, and how their left and right sides compare to each other. To accomplish this, 
 * we decided on anchoring a sensor connected to a resistance band to the base of a door since we saw 
 * this as a convenient location that everyone would have in their homes. Adjacent to the sensor is an 
 * interface that users can use to see the results of their exercises.
 * 
 * 
 * This code takes load cell data, processes it, and displays the results on a  
 * screen. It also responds to user input, by means of thee buttons.
 * Input: 3 buttons, 1 load cell sensor || Output: 1 LCD screen
 * 
 * PIN 2  = Load Cell Sensor Pin CLK
 * PIN 3  = Load Cell Sensor Pin DOUT
 * PIN 8  = Clear Button
 * PIN 9  = Right Button
 * PIN 10 = Left Button
 * 
 */
#include <SPI.h>
#include <Wire.h>
#include <LiquidCrystal_I2C.h>
#include "Queue.h"
#include "HX711.h"

#define DOUT  3 // Load Cell pin 1
#define CLK  2 // Load Cell pin 2
#define BUTX 8 // Clear Button
#define BUTR 9
#define BUTL 10
#define CALIBRATION_FACTOR -7050
#define WINDOW_SIZE 5
#define AVERAGES_SIZE 5

LiquidCrystal_I2C screen(0x27, 20, 4);

HX711 scale;

const char LEFT = 0;
const char RIGHT = 1;

float calibrationFactor = -7050; 
//calibration factor used by HX711 library to do some sort of data filtering
unsigned long startMillis;
unsigned long timerMillis;
unsigned long currentMillis;
int exerciseTimer = 0;
int rightPressed = 1;
int leftPressed = 1;
int clearPressed = 1;
bool exerciseMode = false;
int leftMax = 0;
int rightMax = 0;
bool leftActivated = false;
bool rightActivated = false;
bool leftInUse = false;
bool rightInUse = false;

// window and average queues
Queue<int> window = Queue<int>(WINDOW_SIZE);
int windowSum = 0;
Queue<float> averages = Queue<float>(AVERAGES_SIZE);
float averagesSum = 0;
// variables to help with repetition counting
String oldSlope = "flat";
int repLeftCount = 0;
int repLeftWeightSum = 0;
int repRightCount = 0;
int repRightWeightSum = 0;

//---  HELPERS  ---

void initLoadCell()
{
  scale.begin(DOUT, CLK);
  scale.set_scale(CALIBRATION_FACTOR);
  scale.tare(); //Reset the scale to 0
}

void homeScreen() {
  screen.home();
  screen.clear();
  screen.print("Press L/R to begin");
  screen.setCursor(0, 1);
  screen.print("and end exercises.");
  screen.setCursor(0, 2);
  screen.print("Press X for results.");
  screen.display();
}

void beginExDisp() {
  for (int i = 5; i > 0; i--) {
    screen.home();
    screen.clear();
    screen.print("Start in ");
    screen.print(i);
    screen.print(" seconds");
    screen.display();
    delay(1000);
  }
  screen.clear();
  screen.print("Go!");
  delay(500);
  screen.display();
  startMillis = millis();
  exerciseTimer = 0;
}

void handleButtonSide(char side) {
  beginExDisp();
  if (side == LEFT)
  {
    leftActivated = true;
    leftInUse = true;
    rightInUse = false;
  }
  else
  {
    rightActivated = true;
    leftInUse = false;
    rightInUse = true;
  }
}

void handleButtonClear() {
  // if both the left and right buttons were pressed during 
  // the last exercise set
  screen.clear();
  if (leftActivated && rightActivated) 
  {
    screen.print("Left Max: ");
    screen.print(leftMax);
    screen.setCursor(0, 1);
    screen.print("Reps: ");
    screen.print(repLeftCount);
    screen.setCursor(0, 2);
    screen.print("Avg Weight: ");
    float leftAvg = (float)repLeftWeightSum / (float)repLeftCount;
    screen.print(leftAvg);
    delay(3000);
    screen.clear();
    screen.print("Right Max: ");
    screen.print(rightMax);
    screen.setCursor(0, 1);
    screen.print("Reps: ");
    screen.print(repRightCount);
    screen.setCursor(0, 2);
    screen.print("Avg Weight: ");
    float rightAvg = (float)repRightWeightSum / (float)repRightCount;
    screen.print(rightAvg);
    delay(3000);
    if (!(leftAvg == 0)) {
      screen.clear();
      screen.print("Right : Left = ");
      screen.setCursor(0, 1);
      screen.print(rightAvg);
      screen.print(" : ");
      screen.print(leftAvg);
      delay(3000);
    }
  }
  else if (leftActivated)
  {
    screen.print("Left Max: ");
    screen.print(leftMax);
    screen.setCursor(0, 1);
    screen.print("Reps: ");
    screen.print(repLeftCount);
    screen.setCursor(0, 2);
    screen.print("Avg Weight: ");
    float leftAvg = (float)repLeftWeightSum / (float)repLeftCount;
    Serial.print(leftAvg);
    screen.print(leftAvg);
    delay(3000);
  }
  else if (rightActivated) 
  {
    screen.print("Right Max: ");
    screen.print(rightMax);
    screen.setCursor(0, 1);
    screen.print("Reps: ");
    screen.print(repRightCount);
    screen.setCursor(0, 2);
    screen.print("Avg Weight: ");
    float rightAvg = (float)repRightWeightSum / (float)repRightCount;
    Serial.print(rightAvg);
    screen.print(rightAvg);
    delay(3000);
  }
  else
  {
    screen.print("No reps counted!");
    delay(500);
  }

  // reset globals
  leftMax = 0.0;
  rightMax = 0.0;
  leftActivated = false;
  rightActivated = false;
  leftInUse = false;
  rightInUse = false;
  homeScreen();
  initLoadCell();
  repLeftCount = 0;
  repLeftWeightSum = 0;
  repRightCount = 0;
  repRightWeightSum = 0;
  Serial.println("reset reps");
  
}

//---  MAIN CODE  ---

void setup() {
  Serial.begin(9600);
  pinMode(BUTR, INPUT_PULLUP);
  pinMode(BUTL, INPUT_PULLUP);
  pinMode(BUTX, INPUT_PULLUP);
  
  screen.init();
  screen.backlight();
  screen.print("Welcome, Gary!");
  delay(1000);
  homeScreen();

  scale.begin(DOUT, CLK);
  scale.set_scale(CALIBRATION_FACTOR);
  scale.tare();
  
  startMillis = millis();
  timerMillis = millis();
}


void loop() {
  screen.home();
  scale.set_scale(calibrationFactor);
  
  currentMillis = millis();

  rightPressed = !digitalRead(BUTR);
  leftPressed = !digitalRead(BUTL);
  clearPressed = !digitalRead(BUTX);

  if (clearPressed)
  {
    exerciseMode = false;
    handleButtonClear();
  }
  else if (leftPressed)
  {
    exerciseMode = true;
    handleButtonSide(LEFT);
  }
  else if (rightPressed)
  {
    exerciseMode = true;
    handleButtonSide(RIGHT);
  }
  
  // figure out average from window
  int newReading = abs(scale.get_units());
  window.push(newReading);

  float newAverage;
  if(window.count() >= WINDOW_SIZE)
  {
    int oldReading = window.pop();
    windowSum += newReading;
    windowSum -= oldReading;
    newAverage = float(windowSum) / WINDOW_SIZE;
  }
  else
  {
    windowSum += newReading;
    newAverage = float(windowSum) / window.count();
  }

  // figure out up/down from average
  averages.push(newAverage);

  float oldTotal = averagesSum;
  float newTotal;
  
  if(averages.count() >= AVERAGES_SIZE)
  {
    float oldAverage = averages.pop();
    averagesSum += newAverage;
    averagesSum -= oldAverage;
    newTotal = averagesSum;
  }
  else
  {
    averagesSum += newAverage;
    newTotal = averagesSum;
  }

  // decide if up/down, adjust vars
  // figure out the new slope
  float difference = newTotal - oldTotal;
  String newSlope = "";
  if (difference <= 0.02 && difference >= -0.02)
  {
    newSlope = "flat";
  }
  else if(difference >= 0.02)
  {
    newSlope = "positive";
  }
  else if(difference <= -0.02)
  {
    newSlope = "negative";
  }

  // decide if a repitition was performed
  if (newSlope == "negative" && oldSlope == "positive")
  {
    if (leftInUse)
    {
      repLeftCount++;
      repLeftWeightSum += newReading;
      Serial.println("left side reading: ");
      Serial.print(newReading);
    }
    else if(rightInUse)
    {
      repRightCount++;
      repRightWeightSum += newReading;
      Serial.println("Right side reading: ");
      Serial.print(newReading);
    }
  }
  oldSlope = newSlope;

  // update maximum reading
  if (leftInUse && (leftMax < newReading))
  {
    leftMax = newReading;
  }
  else if(rightInUse && (rightMax < newReading))
  {
    rightMax = newReading;
  }
  if (exerciseMode) {
    if (currentMillis - timerMillis >= 1000) { // keep track of time passed while exercising
      exerciseTimer++;
      screen.clear();
      screen.print("Time: ");
      screen.print(exerciseTimer);
      screen.print(" seconds");
      screen.setCursor(0,1);
      screen.print("Working ");
      if (leftInUse) { screen.print("left "); }
      else { screen.print("right "); }
      screen.print("side.");
      screen.display();
      timerMillis = millis();
    }
  }
}

Schematic

Final Design DXF