How to: Ultrasonic Bench Press and Squat Rep Counter – ESP8266
How to: Ultrasonic Bench Press and Squat Rep Counter – ESP8266
How can we prove my honesty and simultaneously resolve the issue of losing my rep count? There could be only one solution: create an automatic rep counter device using Arduino…count barbell reps! But, in order to do this, we’d need a few different components to work together in order to get it right.
More likely than not, you’ve found that exercising with a partner is not as common as it used to be. Unless you’re lucky enough to have access to private gym equipment or a home gym. You know by now that counting your reps out loud to yourself can get boring and can be inaccurate. Scouts Honor, I’ve never skipped a rep – though I hear some do.
Disclosure: this blog contains affiliate links that help support this content. If you enjoy these posts, consider using the links while shopping online, with no extra cost to you.

Which Parts were used in this device?
Ultrasonic HC-SR04 Module
Using the HC-SR04 is a trade-off. We gain simplicity in the overall design, but, adds spacial constraints around the area that can be used to count a rep. Although using a laser seems enticing, it requires a bit more setup. In order to keep the device portable, by avoiding the combination of a KY-008 and laser receiver we count each rep from a single direction vs detecting the laser interruption.
Runner-up Sensor (first attempt)
At first, I thought it would be nice to use a sensor that I already had lying around. Everyone has a few PIR sensors floating here and there. For counting reps, the sensor had two problems.
- The detection radius is too large
- there is a 3-second reset before the sensor can be triggered again. Unless you are doing pause reps, this would be a problem.
Why not give it a try?
The solution to problem #1: Print a box that will shield a portion of the sensor from detecting movement. That actually worked very well. Here’s the STL for the PIR Limiter Box.
The solution to problem #2: Choose a new sensor.
I2C IIC OLED (.096″) Display
By attaching a small display, it’s easy to see the current rep at a glance and even label the current lift with a title. Each time our bar passes the HC-SR04, the number on the display increments.
ESP8266 NodeMCU
Picking the ESP8266 NodeMCU as the backbone for this project is a no-brainer. Not only is this a small board, but, it also exposes ample GPIO pins for connecting to the OLED display and Ultrasonic module without requiring a breakout board. Although it’s not being used in this project, the ESP8266 supports WiFi and could easily broadcast information to a web service or MQTT server.
What about the enclosure?!
Because this is a one of a kind build, it requires a one of a kind enclosure. For this, we’ll get out our 3D modeling software and design something compact to hold our components and keep them oriented.
I used TinkerCad and am in no way an expert at creating 3D models. It was a miracle that this one turned out.
Here are links to the STL if you’re interested in using them:
This case is designed so that the screen displaying the current rep faces toward the user, while the ultrasonic module faces outward, looking for the barbell to pass by (twice).
In order to mount the box to the rack, a few adhesive-backed magnets are used – covering the small friction door.
Next Steps?
Currently, I’m in the process of testing various mounting points on the rack and revisiting the source to improve the accuracy of the count. At this time the accuracy is ~90% after attempting 150 reps.
Source Code
This code is still a work in progress. There are surely some features to discover. At this time, the current rundown is like this:
- Set a flag when the barbell passes within the specified range
- If the barbell passes a second time and at least 200 ms has elapsed, count the rep
- If an object is detected in range and remains there for a few seconds, clear the current rep count and reset to start counting again
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define SCREEN_WIDTH 128 // OLED display width, in pixels
#define SCREEN_HEIGHT 64 // OLED display height, in pixels
//#define SENSOR 2 // PIR Sensor
#define ECHO 12 // ultrasonic input
#define TRIG 14 // ultrasonic output
// Declaration for an SSD1306 display connected to I2C (SDA, SCL pins)
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);
int initRep = 0;
int currentRep = 0;
int numberOfLifts = 3;
int currentLift = 0;
int previousLift = 0;
char buffer [16];
long distance = 0.0;
long prevDistance = 0.0;
long duration;
int hoverTimer = 0;
int firstMillis;
int secondMillis;
boolean firstPass = false;
//int sensorBuffer = 0;
void setup() {
Serial.begin(115200);
if(!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) { // Address 0x3D for 128x64
Serial.println(F("SSD1306 allocation failed"));
for(;;);
}
//Define inputs and outputs
pinMode(TRIG, OUTPUT);
pinMode(ECHO, INPUT);
//pinMode(SENSOR, INPUT);
delay(2000);
}
void loop() {
distance = readDistance();
if(distance > 0.0 && distance < 18.0){
if(prevDistance > 18.0 && firstPass == false){
firstPass = true;
firstMillis = millis();
} else if(prevDistance > 18.0 && firstPass == true){
if((millis() - firstMillis) > 200){
currentRep++;
firstPass = false;
}
}
}
prevDistance = distance;
if(distance < 18.0 && prevDistance < 18.0){
hoverTimer++;
} else {
hoverTimer = 0;
}
if((currentLift != previousLift) || (hoverTimer > 50)){
hoverTimer = 0;
resetReps();
}
display.clearDisplay();
if(currentLift == 0){
printWorkout("Bench Press");
previousLift = 0;
} else if(currentLift == 1){
//printInclineBench();
} else if(currentLift == 2){
//printSquat();
}
delay(50);
}
void printWorkout(String name){
display.setTextSize(1);
display.setCursor(0, 0);
display.println(name);
display.setTextSize(6);
display.setTextColor(WHITE);
display.setCursor(35, 20);
// Display static text
sprintf (buffer, "%02u", currentRep);
display.println(buffer);
display.display();
//currentRep++;
}
void resetReps(){
currentRep = initRep;
}
float readDistance(){
// The sensor is triggered by a HIGH pulse of 10 or more microseconds.
// Give a short LOW pulse beforehand to ensure a clean HIGH pulse:
digitalWrite(TRIG, LOW);
delayMicroseconds(5);
digitalWrite(TRIG, HIGH);
delayMicroseconds(10);
digitalWrite(TRIG, LOW);
// Read the signal from the sensor: a HIGH pulse whose
// duration is the time (in microseconds) from the sending
// of the ping to the reception of its echo off of an object.
pinMode(ECHO, INPUT);
duration = pulseIn(ECHO, HIGH);
// Convert the time into a distance
//cm = (duration/2) / 29.1; // Divide by 29.1 or multiply by 0.0343
return (duration/2) / 74; // Divide by 74 or multiply by 0.0135
}