Click here to Skip to main content
15,867,928 members
Articles / Internet of Things / Arduino

Arduino - Exercise Reps Counter with Analog Voltage Divider Button Controller

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
25 Sep 2020CPOL30 min read 14.6K   297   9   9
An Arduino project that keeps track of your best Workout times and rep-counts
A compact Arduino Uno Exercise Reps-Counter that fits inside a Euro-pack. Push yourself to better your rep-count at the gym and finally be ready for the big day when you assume your rightful place and finally don your boss's favorite fluffy foam mascot costume at the company picnic next year. You can do it! With the right motivation, proper diet and ... this Arduino Reps Counter. Uses Ultrasonic Sensor, LCD1602 and an Analog Voltage Divider button control system that can detect up to 128 buttons using only 1 Analog pin(soldering mastery not included).

Introduction

Since today is the anniversary of the day I changed my life-insurance policy's sole-beneficiary to my beloved Arduina, I thought it would be a good day to write this article. And it also coincides with the end (major milestone) of my latest Arduino build, the Exercise Reps-Counter. It took me only one day to bread-board, test and build the first working prototype. It was pretty basic: you do a burpie, it counts a burpie and keeps track of your time. I've rebuilt it twice since then (because of a hot-glue-meets-heat-sensitive-resistors incident) but spent most of the next two weeks working on the software.

Background

I never really used to exercise until my mid thirties when I was in the penitentiary and I noticed my torso was getting wider in all directions.

I convinced myself,                                                                              

                "It's my age. This is normal. I'll just have to get used to it." 

But that thought didn't stick.

Nor should it have. Instead, I started doing burpies by myself in my concrete cell. The first time, I had a mind to do three sets of thirty and wound up nearly having a heart-attack after only two sets. I took a break for a week to recover and did it again, and once more a few days later. Soon, I was doing three sets on alternate days increasing the reps per set by 5 reps once a week.

That was fourteen years ago. Now I have calluses at the base of my palms where my hands slap the floor and I feel great. I have more wind than ever and don't get out of breath like I used to. Most everything I do is helped by my improved health and any other exercise I do is a little easier because of these calisthenics.

You can do burpies anywhere: locked-down in a prison cell, quarantined on a Princess cruise ship, and they might even let you do a set at your local gym. Even if all the equipment in the rec-hall is being monopolized by the steroid-and-protein-shake-between-meals guys, you still have a spot on the floor. And when that loud mouth at the punching bag bragging about how he plans on hogging it for himself for the entire period, starts banging on it for a thump or two, you can sit back while you tie your wraps around your wrists because you know that he'll soon be out of breath, panting, gasping and wheezing while he pitter-patters weakly until he slumps away in embarrassment too gased-out to even tell you he's done before you've even finished with your wraps.

It happens all the time.

But why do you need an Exercise Rep-Counter? If you're like me, no one you know trains hard enough to keep up with you and the only reason they invite you to train with them is to slow you down... or so it seems. I don't play a professional sport and can't afford a personal trainer (even if I wanted one) so it's just me, my sweat and I. You don't just start doing hundreds of burpies overnight and there's a lot of discipline involved in getting there when everyone around you assumes 15 reps is good enough. But for all these years that I've been doing sets of hundreds of burpies, I've been spending that time counting. One... two... three .. and so forth. On and on for the entire workout and it gets a little boring when I could be concentrating on something a little more productive like my next Arduino project.

And thus was born the Exercise Rep-Counter. No more counting! And that's only the least of the benefits it brings. With this tool counting for me and keeping track of my best times, I know where I'm at in my set and can push myself to go faster and do more to feel even better. It's great. Since I made this thing, my stats are 5%-10% better than before. No protein bars, or special drinks involved. Just a bit of motivation and the freedom of mind to not have to count any more. It's great.

Watch a short 15 minute video about my project and see what you think.

Parts

  • Arduino Uno, and USB cord $5.00
  • Ultrasonic Sensor HC-SR04 $1.50
  • LCD 1602 $2.50
  • 5 KΩ potentiometer $1.00
  • 2 LEDs (green & red) $0.50
  • 2x8 cm solder matrix $0.50
  • 5 pushbuttons $0.50
  • DC power switch, Battery & jack (to take it on the road) $10.00
  • various resistors $2.00
  • (solder-less) wires $2.00
  • Dollarama tupperware $1.00
  • solder, soldering iron, tweezers and soldering station,

Total cost on eBay is less than $30.

How It's All Connected

The TinkerCad schematic below shows you how to wire it all together.

Image 1

Connecting this on a bread board shouldn't give you too much trouble. A basic rule of thumb I have neglected on several occasions is to not recognize that LEDs have next to no resistance and need a resistor to be placed in series with them. Newbies (like me) tend to look at all these parts and worry that they'll mis-wire their LCD or Ultrasonic sensor. And they're right to be careful but shouldn't overlook some basic fundamentals about other widgets like the LEDs that look so simple. LEDs have only two legs and their bright colors make them look like chewable candy so it's easy to forget that those two legs are very specifically anode and cathode, and don't mix them up. Sure they look like they could be twins and you wouldn't be alone to think Tweedle Dee looks a bit like Tweedle Dum but if you wire them backwards, you don't get those shiny bright lights and might go looking elsewhere for a solution to your problem when it's really quite basic. The long leg is the anode (positive voltage) and the short leg is the cathode (negative voltage). And don't forget to add a resistor!

If you want to connect it to a battery, you'll only need to plug your battery into the Uno using a regular battery jack.

update: 20200929 - if you're building your own, you may have to reset your EEPROM in order to save/load workouts.  To do so, you merely need to add the following line to your Setup()

EEPROM.update(0,0);

upload your code with this line then remove it and upload your code again.  Your EEPROM will be ready to be programmed.

Voltage Divider Button Controller

What I like about this button controller is that all five buttons are connected to a single analog pin on the Arduino Uno. You could use the same technique to control up to 128+ buttons using only 1 analog Arduino pin.

What's a Voltage Divider?

You can read about the Voltage Divider on Wikipedia, but I'll try to summarize. When current runs through a resistor, the voltage across that resistor drops by a value equal to the current multiplied by the resistance.

Image 2 eq. 1

where V is voltage, I is current and R is resistance

When current flows through two resistors in series, then the voltage drops twice (once for each resistor) and when you measure the voltage between those two resistors, you're using a voltage divider.

Image 3

By changing the values of Z1 and Z2, you can control the voltage measured at Vout.

The schematic below shows you an alternate method of connecting your buttons to your Arduino which is equivalent to the method described in the TinkerCad schematic of the Reps Counter seen above.

Image 4

You can see in the schematic above where the buttons are all connected in series with their own resistors. Each of these (Rn) resistors serves the same purpose as Z1 in the previous diagram and the one common resistor (100Ω at the bottom which I'll refer to as Rc) is equivalent to the resistor Z2. The difference here is that they each have a button that prevents them from going to the output node or even reaching ground. If you recall that the voltage drop across a resistor is equal to the current flowing through it multiplied by the resistance of that resistor, then you will recognize that in the diagram above when none of the buttons are pressed, there is no path from the voltage source Vin to ground and therefore no current. If there is no current, there is no voltage difference across the 100ohm resistor and the voltage measured at Vout is equal to 0 V as if it were tied directly to ground.

When a button is pressed, the voltage Vout can be calculated by first determining the current flowing across both resistors (the one tied to the button that is pressed and the common 100 ohm resistor tied to ground) which we can do with the following formula.

Image 5            from eq. 1

The total voltage drop across both resistors is Vin - ground (0 V) = Vin. And the total resistance if Z1 + Z2.

So we get:

Image 6               eq.2

Now, we calculate the voltage drop across Z1 by multiplying the current by the resistance of Z1 then we subtract that voltage drop from the input voltage and we get:

Image 7     eq.4

How Does This Change the Price of Solder Wire?

You might want to ask that question if your name is Mark Carney but if you're just a newbie widgets and Arduino guy or gal, then the real question is why is the Voltage Divider such an important part of this article?

Here's where we talk about the Voltage Divider Button Controller. My dear Arduina is so fine I'd elect her for president if I could but my beloved Arduina was born in Italy and that makes her ineligible, which is too bad. But the thing about an Arduino, or any other microcontroller, is that they have a limited number of pins you can connect peripherals to. In the case of inputs, like the buttons in this project, you could use 74HC165 Load Registers daisy-chained together to control an unlimited number of buttons using only three of your Arduino pins. Load Registers are definitely very useful and not that complicated to use but they do require three input pins where a carefully crafted voltage divider can control up to 128+ buttons using only one analog pin. There's a lot of soldering involved and figurin' and a thinkin' to get the resistors right, but if you put in the effort, it's not too bad. The drawback, besides all the soldering fun you'll have putting it together, is that if more than one button is pressed at the same time the Voltage-Divider Button Controller has no way of knowing it and will tell you what it thinks given the resultant voltage it reads when two button-resistors dropped that voltage in parallel where the button controller is hard-wired to identify only one button's voltage drop. For that reason then, this particular input controller is not the best for applications which require users to press more than one button at a time, like games in general. The way a Voltage-Divider Button Controller can control up to 128+ buttons on a single line is by evenly dividing the results of Vout (from previous section) across the span of the Arduino's working voltage of 5V. When you read the voltage across an analog pin using the command analogRead(pinNumber) it will give you a value between 0-1023 which is a linear equivalent of the microcontroller's working voltage, that is from 0 to 5V DC. So the resolution is somewhat lacking since the actual voltage has an infinite range of values between 0.0V and 5.0V but can only be expressed by the microcontroller as integral values from 0 to 1023. Since your current may vary and the voltage may fluctuate due to the imperfections of little electronic widgets like resistors having something called a tolerance value, you can't expect to produce a Voltage Divider Button Controller using all 1024 possible values of voltage that can be read by the analogRead() function. For this reason then, by my confounded guesstimation, limiting yourself to a maximum of 128 buttons means you have a bit of leeway between them. Now, you may wonder, "how is he going to calculate the resistance values for 128 buttons along with their one common resistor?" And that's a fair question, but it's really not that complicated.

If you have five buttons (as we do in this project), then evenly dividing them across some input voltage Vin (here 5 volts) means you need to leave an even gap above and below each button's voltage value creating six 'regions' of equal sized voltage differences.

Image 8

So you have to divide Vin by one plus the number of buttons, in this case (1+5)=6. You could do without the extra gap either above or below but I like the symmetry.

You choose a common Resistor (Z1 in previous references).

Then you do some algebra with equation 4 (transmogrifying the generic Zs into more mnemonic friendly Rs).

Image 9

and then run the voltage values through this to calculate the five different values of R1 for each of the buttons and you're done.

That's all fine and good when you're only dealing with 5 buttons but when you plan on wiring 128 buttons to the same analog pin, you'll probably need to be a little more efficient and that's what we have computers for. Let them do all the work, I say. So now that I have put a few hours of labour into writing the C# application to do all this for me, I can finally sit back and know I've given my common-law laptop computer (who has been feeling depressed since my beloved Arduina has come into my life) something to do to while away the hours of neglect.

Suppose we change things around and place the common resistor above the buttons and their resistors. This way, the voltage drop across any one button's resistor will be equal to the voltage at the output node and we can simplify our calculations.

If we know the number of buttons we want, then the output voltage at any given Vn for the nth button is equal to a fraction of the input voltage determined by:

Image 10

where Bn is the number of buttons in our voltage divider and n is the button 'ID' number(ranging from 0 to Bn -1) for which we are calculating the needed resistance in order to produce the desired output Vn for that button.

Using a simplified voltage divider equation that gives us the voltage Vout as the voltage dropping across the second resistor before it reaches ground:

Image 11

where Rn is below Vout and is tied to ground.

Given two equations for the same output voltage of Vn we can let them equal each other and remove Vin altogether to get:

Image 12

When you do the algebra, it turns out that our longed for resistance value is equal to:

Image 13

Nothing to it, right? But it is still a hassle when you have several dozen resistors to account for.

So I made a C# app to do all this work for me.

The download zip file includes a compiled executable file you can look for if you don't have C# downloaded and ready to compile this app yourself.

c:\\Voltage Divider Resistance Calculator\\bin\\debug\\Voltage Divider Button Controller.exe

Here's a screen capture. It's simple to use and user-friendly. You just pick a common resistance value and the number of resistors you want. Once it has calculated the resistances, you can save them to a text file or read them off the screen. It will generate the Arduino code necessary for your project to decide which button has been pressed and, if you ask it nicely, it'll even draw you a picture showing you how to wire it up.

Image 14

And here's the code it wrote which you can use to start your project with:

C#
const int pinBtns = A0;
int intBtnOLD = -1;

int Buttons_Handle()
{
  int intVoltageInput = analogRead(pinBtns);
  if (intVoltageInput > 114 && intVoltageInput < 226)
    return 0;
  else if (intVoltageInput > 285 && intVoltageInput < 397)
    return 1;
  else if (intVoltageInput > 456 && intVoltageInput < 568)
    return 2;
  else if (intVoltageInput > 626 && intVoltageInput < 738)
    return 3;
  else if (intVoltageInput > 797 && intVoltageInput < 909)
    return 4;
  else return -1;
}

void setup()
{
  Serial.begin(9600);
  pinMode(pinBtns, INPUT);
}

void loop()
{
  delay(5);
  int intBtnPressed = Buttons_Handle();
  if (intBtnPressed != intBtnOLD)
  {
    intBtnOLD = intBtnPressed;
    Serial.println(intBtnPressed);
  }
}

N.B. The button values assigned to the voltages detected are actually reversed. It's not a bug ... it's a feature! If you wired it expecting the 0th button to be, say, on the left of your controller, but it's actually on the right, well... then that's my fault. But you should run the code and see which button is which. The error I made was in using the formula for the schematic with the buttons tied to Vin and the common resistor tied to ground (not the one the app draws for you). ... I'm not even sure anymore. Tomato ... potato. Just test the buttons first and then write your code around it.

It knows what the voltage is to be expected from each button being pressed and calculates one third of the way to the adjacent button's voltage and uses that value as a tolerance range. Its like it cut each range of voltage between one button and the next by three and assigned the first third to that button and the last third to the next button, thereby leaving a gap of 1/3 the difference between two adjacent voltage levels unassigned to either button so there's a bit of leeway and no confusion as to which button was actually pressed..

It'll even draw you a picture.

Image 15

You may not want to put it on your wall but, if your laptop is feeling a little neglected like mine is, you might want to stick it to your fridge to brighten things up a little.

What's in a Menu?

Menus... these gave me trouble. The thing about menus is that you have to be able to read them for them to be of any use. And the thing about reading is that in Arduino, reading requires a lot of text and text is expensive. When you play with an Arduino, it's like you're going back in time with regards to how free you are to gobble up oodles of memory. You see, Arduino has a bad side that you just have to learn to cope with. It's not that it's temperamental, it's just that it couldn't be bothered to tell you when the variables you've declared consume all of the memory you have available for variables. That's when variables cease to be declared or assigned, and even stop having significance altogether. And, of course, when that happens, your code doesn't run quite right. Don't expect your Arduino to do anything about it because that's your problem. You could take it out on her but it's really not her fault. She's not the boss of you, you know. So, do what you want but don't expect your Arduino to tell you at what point things went south when they do because they will if you're not careful with your memory consumption.

Just know that memory is an issue.

When you compile your Arduino project, it will tell you how much memory you're using. The image below shows what my IDE says when the Reps Counter.ino file is uploaded to the Uno.

Image 16

You can see that the software takes up 91% of its available memory and the global variables take up 48% which leaves me with 1047 bytes of memory for variables declared during runtime. That's really not a lot of memory. And the reason why I say this here is because the menus that are needed for the safe operation of this important advanced fitness tool, which I call the Reps Counter, print a lot of text on the screen. And text is expensive in terms of memory. Each character may only be one byte but when I built my first iteration of the menus, each menu item was an instance of a class and kept the text it needs to put on the LCD screen in RAM memory. There are 27 different menu items and each one is almost 16 characters in length, so that adds up to 351 bytes. And ok, that still doesn't sound like much but Arduino's memory ... not so good. And as I went along, I kept adding menu options and building on this problem until eventually it wasn't just a minor glitch to be ignored because eventually everything crashed, things weren't being printed right and it was what you might call a problem. So, I went onto the Arduino Forum and asked a silly question about infinite-loops-that-shouldn't-be and got replies that made for interesting reading but didn't solve my problem because the problem had nothing to do with the infinite-loop-that-shouldn't-be symptom but rather had to do with the oodles (... 351 bytes!) of memory being used up by the menus class. To fix that problem, I removed the text from that class and instead wrote a function that creates a temporary string of the needed text just before it is rushed to the LCD screen where it lives in belighted splendour in all its glory while that temporary string that delivered the text there safely is immediately destroyed, like something out of a Mission Impossible movie, as I watch on benignly.

So, as fun as it is to plug things together and make something out of electronic widgets, the Arduino tends to be silent when it comes to problems in your code.

If you're used to C# and Microsoft's Visual Studio in general, then you've been spoiled with the means to look at every variable at any time and watch the clockwork from the inside as you write and debug your code. But with Arduina, its different. She's a little shy about showing me what she's doing. So I have to gently ask questions and slowly get her to reveal her secrets little by little. And, of course, whenever I do add those informative little debugging lines of code telling my Arduina to report on the state of things, those little lines of code gobble up oodles and oodles of memory potentially compounding whatever memory problem I had in the first place.

But I love her.

One last thing before I detail how these menus work, whenever you're writing a string in Arduino, you need to tell the IDE and your project to put that string in PROGMEM. Otherwise it assumes you mean it to be stored both as a variable AND program memory. Strings are doubly hungry for memory that way. There's a simple way to get around this and you can try typing the UFC Rules of the Octogon as a text string in your code before and after using this trick and you'll immediately see the difference when you compile it, so there's no need to fight.

The F() macro is so simple to use but what does do exactly? Before someone figured out what to do with the string memory issue, coders had to explicitly tell the IDE to put strings in PROGMEM and it was a major hassle and then some guy, whose name I can't recall, wrote a simple solution to this major downside to the Arduino. Its a Macro (with a capital M, thank you) that gets pre-processed before the IDE compiles your code and it ensures that any string you have tucked away within it is stored in PROGMEM. This way, you save those precious 2048 bytes of variable memory you have for something you really need it for and there are no surprises.

In the code below, you can have a look at the function I mentioned earlier that creates a string variable to print text onto the LCD screen before that string is destroyed. You'll notice that each string being returned to the calling function is encapsulated within the F( ) Macro.

C#
String mnuDraw_Heading(int intIndex)
{
  switch (intIndex)
  {
    case 0: return F("Menu");
    case 1: return F("View Workout");
    case 2: return F("Edit Workout");
    case 3: return F("Edit Name");
    case 4: return F("Select type");
    case 5: return F("SW - CountUP");
    case 6: return F("SW - CountDown");
    case 7: return F("Tmr - CountUp");
    case 8: return F("Tmr - NoCount");
    case 9: return F("Edit Trigger");
    case 10: return F("Sel trig type");
    case 11: return F("Distance");
    case 12: return F("Time delay");
    case 13: return F("set distance");
    case 14: return F("min");
    case 15: return F("reset distance");
    case 16: return F("trigRst delay");
    case 17: return F("Edit Next");
    case 18: return F("File");
    case 19: return F("new");
    case 20: return F("load");
    case 21: return F("delete");
    case 22: return F("Preferences");
    case 23: return F("default workout");
    case 24: return F("toggle sound");
    case 25: return F("Workout");
    case 26: return F("System");
  }
}

When this macro is removed and the strings are left naked for the IDE to have its way with the project compiles just the same but uses more of its limited Variable memory as shown below:

Image 17

Compared to with the F() Macro:

Image 18

Since we have 32Kilobytes of program memory, the 30 bytes added by the F() macro do not affect the project's memory consumption so much as the 258 bytes that were used up of your measly 2048 bytes available for variables. Global variable memory usage dropped from 1259 to 1001 simply by using this pre-processing macro.

So, remember kids, Arduino safely. And always wear your F( ) Macro!

Menus

Of course, we have to talk about these famous menus. The menus are implemented using the simple-looking class shown below:

C#
class classMenuItem
{
  public:
    classMenuItem *cParent = NULL;
    classMenuItem *cFirstChild = NULL;
    classMenuItem *cPrevSibling = NULL;
    classMenuItem *cNextSibling = NULL;
    enuMenuAction eMenuAction = mnuAction_NoAction;
    byte Index;
};

I call it simple-looking because it only has zero lines of code and all of half a dozen variable declarations.

These variables consist mostly of pointers to other instances (2 bytes of memory each) of the same class, one byte records what action to take when the user makes this selection and the last byte identifies the instance of the class in order to properly select which text belongs on the screen when it needs to be printed.

And that's it.

Now we only have to plug them up together and tell them what to do because that's where all the fun is.

Since we're using pointers to other instances of this class, you've probably guessed that the menu system is really just like one friendly data-tree. And it is. You have the root of the tree and the connecting branches reach out in all their evergreen glory (deciduous trees would make a mess of things, thank you).

Here's a picture of what it looked like in my head as I conceived it before writing the code that connects them all together (which you can read further down):

Image 19

You can see that it sort of looks like a tree with 0-Menu being the root. I kept this information in a text-file on my harddrive and referred to it everytime I needed to make changes to the code. First, I edited the textfile (image of menu tree above without the orange and green arrows) and then went over each entry to make sure the XML-ish "first-child" & "next-sibling" business reflected what the tree was supposed to look like and wrote the function below which is called by setup() and builds the menu-data-tree.

C#
void Menus_init()
{
  cMenus[0].cFirstChild = &cMenus[1];
  cMenus[0].cNextSibling = NULL;
  cMenus[0].eMenuAction = mnuAction_NoAction;

  cMenus[1].cFirstChild = NULL;
  cMenus[1].cNextSibling = &cMenus[2];
  cMenus[1].eMenuAction =   mnuAction_ViewCurrentWorkout;

  cMenus[2].cFirstChild = &cMenus[3];
  cMenus[2].cNextSibling = &cMenus[18];
  cMenus[2].eMenuAction = mnuAction_NoAction;

  cMenus[3].cFirstChild = NULL;
  cMenus[3].cNextSibling = &cMenus[4];
  cMenus[3].eMenuAction = mnuAction_Workout_EditName;

  cMenus[4].cFirstChild = &cMenus[5];
  cMenus[4].cNextSibling = &cMenus[9];
  cMenus[4].eMenuAction = mnuAction_NoAction;

  cMenus[5].cFirstChild = NULL;
  cMenus[5].cNextSibling = &cMenus[6];
  cMenus[5].eMenuAction = mnuAction_WorkoutSelection_StopWatch_CountUp;

  cMenus[6].cFirstChild = NULL;
  cMenus[6].cNextSibling = &cMenus[7];
  cMenus[6].eMenuAction = mnuAction_WorkoutSelection_StopWatch_CountDown;

  cMenus[7].cFirstChild = NULL;
  cMenus[7].cNextSibling = &cMenus[8];
  cMenus[7].eMenuAction =   mnuAction_WorkoutSelection_Timer_CountUp;

  cMenus[8].cFirstChild = NULL;
  cMenus[8].cNextSibling = NULL;
  cMenus[8].eMenuAction = mnuAction_WorkoutSelection_Timer_NoCount;

  cMenus[9].cFirstChild = &cMenus[10];
  cMenus[9].cNextSibling = &cMenus[17];
  cMenus[9].eMenuAction = mnuAction_NoAction;

  cMenus[10].cFirstChild = &cMenus[11];
  cMenus[10].cNextSibling = &cMenus[13];
  cMenus[10].eMenuAction =   mnuAction_NoAction;

  cMenus[11].cFirstChild = NULL;
  cMenus[11].cNextSibling = &cMenus[12];
  cMenus[11].eMenuAction =   mnuAction_SelectTriggerResetType_Distance;

  cMenus[12].cFirstChild = NULL;
  cMenus[12].cNextSibling = NULL;
  cMenus[12].eMenuAction = mnuAction_SelectTriggerResetType_TimeDelay;

  cMenus[13].cFirstChild = &cMenus[14];
  cMenus[13].cNextSibling = &cMenus[16];
  cMenus[13].eMenuAction =   mnuAction_NoAction;

  cMenus[14].cFirstChild = NULL;
  cMenus[14].cNextSibling = &cMenus[15];
  cMenus[14].eMenuAction = mnuAction_TriggerDistance_SetMin;

  cMenus[15].cFirstChild = NULL;
  cMenus[15].cNextSibling = NULL;
  cMenus[15].eMenuAction = mnuAction_TriggerDistance_SetResetTolerance;

  cMenus[16].cFirstChild = NULL;
  cMenus[16].cNextSibling = NULL;
  cMenus[16].eMenuAction = mnuAction_TriggerTimerDelay_Set;

  cMenus[17].cFirstChild = NULL;
  cMenus[17].cNextSibling = NULL;
  cMenus[17].eMenuAction = mnuAction_Workout_EditNext;

  cMenus[18].cFirstChild = &cMenus[19];
  cMenus[18].cNextSibling = &cMenus[22];
  cMenus[18].eMenuAction =   mnuAction_NoAction;

  cMenus[19].cFirstChild = NULL;
  cMenus[19].cNextSibling = &cMenus[20];
  cMenus[19].eMenuAction = mnuAction_EEPROM_New;

  cMenus[20].cFirstChild = NULL;
  cMenus[20].cNextSibling = &cMenus[21];
  cMenus[20].eMenuAction = mnuAction_EEPROM_Load;

  cMenus[21].cFirstChild = NULL;
  cMenus[21].cNextSibling = NULL;
  cMenus[21].eMenuAction = mnuAction_EEPROM_Delete;

  cMenus[22].cFirstChild = &cMenus[23];
  cMenus[22].cNextSibling = NULL;
  cMenus[22].eMenuAction = mnuAction_NoAction;

  cMenus[23].cFirstChild = NULL;
  cMenus[23].cNextSibling = &cMenus[24];
  cMenus[23].eMenuAction = mnuAction_Preferences_DefaultWorkout;

  cMenus[24].cFirstChild = &cMenus[25];
  cMenus[24].cNextSibling = NULL;
  cMenus[24].eMenuAction = mnuAction_NoAction;

  cMenus[25].cFirstChild = NULL;
  cMenus[25].cNextSibling = &cMenus[26];
  cMenus[25].eMenuAction = mnuAction_Preferences_ToggleSound_Workout;

  cMenus[26].cFirstChild = NULL;
  cMenus[26].cNextSibling = NULL;
  cMenus[26].eMenuAction = mnuAction_Preferences_ToggleSound_System;

  // set cParent & cPrevSibling
  for (int intMnuCounter = 0; intMnuCounter < bytMenus_Num; intMnuCounter ++)
  {
    cMenus[intMnuCounter].Index = (byte)intMnuCounter;
    classMenuItem *cMnu = &cMenus[intMnuCounter];
    if (cMnu->cFirstChild != NULL)
    {
      classMenuItem *cChild = cMnu->cFirstChild;

      do
      {
        cChild->cParent = cMnu;
        cChild = cChild->cNextSibling;
      } while (cChild != NULL);
    }

    if (cMnu->cNextSibling != NULL)
      cMnu->cNextSibling->cPrevSibling = cMnu;
  }

  /*
    for (int intCounter = 0; intCounter < bytMenus_Num; intCounter ++)
    debug_Print_Menu(intCounter);
    //*/
}

The instances are declared in a global variable called cMenus[] and initialized here with their FirstChild and NextSibling values set by hand while all of their Parent and prevSibling pointers are set by the Arduino to reflect the values that were written by hand. Letting the Arduino do this itself avoids possible data-entry errors and would be redundant to write by hand anyways.

The general idea is that each instance points to its immediate neighbours in the tree. There is only one selection whose Child-Menus are visible on the screen and when you make a selection of one of those Child-Menus then that child-menu is used as the current menu if it has a mnuAction_NoAction action associated with it otherwise the mnuSelect() function sorts it all out and redirects the program flow according to the mnuAction that is associated with the selected instance of the menu class as shown in the code below:

C#
void mnuSelect()
{
  switch (cMnu_Selection->eMenuAction)
  {
    case mnuAction_WorkoutSelection_StopWatch_CountUp:
      {
        cWorkout.eWorkoutType = StopWatch_CountUp;
        EEPROM_eWorkoutType_Save(cWorkout.intIndex, cWorkout.eWorkoutType);
        mnuDraw();
        FlashMessage(F("SW - CountUp"));
        eSong = eSong_Confirm;
      }
      break;

    case   mnuAction_WorkoutSelection_StopWatch_CountDown:
      {
        cWorkout.eWorkoutType = StopWatch_CountDown;
        cWorkout.uintRepCounter = EditValue("Rep Count:", cWorkout.uintRepCounter, 1, 16000);
        cWorkout.ulngBest = 99 * conMillisPerHour
                            + 59 * conMillisPerMinute
                            + 59 * conMillisPerSecond;
        EEPROM_eWorkoutType_Save(cWorkout.intIndex, cWorkout.eWorkoutType);
        EEPROM_uintRepCounter_Save(cWorkout.intIndex, cWorkout.uintRepCounter);
        EEPROM_Best_Save(&cWorkout, cWorkout.ulngBest);
        mnuDraw();
        FlashMessage(F("SW - CountDown"));
        eSong = eSong_Confirm;
      }
      break;

    case   mnuAction_WorkoutSelection_Timer_CountUp:
      {
        cWorkout.eWorkoutType = Timer_CountUp;
        cWorkout.ulngBest = 0;
        EditWorkout_Time();
        EEPROM_eWorkoutType_Save(cWorkout.intIndex, cWorkout.eWorkoutType);
        EEPROM_Timer_Save(&cWorkout);
        EEPROM_Best_Save(&cWorkout, cWorkout.ulngBest);
        mnuDraw();
        FlashMessage(F("Timer CountUp"));
        eSong = eSong_Confirm;
      }
      break;

    case   mnuAction_WorkoutSelection_Timer_NoCount:
      {
        cWorkout.eWorkoutType = Timer_NoCount;
        EditWorkout_Time();
        EEPROM_eWorkoutType_Save(cWorkout.intIndex, cWorkout.eWorkoutType);
        EEPROM_Timer_Save(&cWorkout);
        mnuDraw();
        FlashMessage(F("Timer NoCount"));
        eSong = eSong_Confirm;
      }
      break;

    case   mnuAction_SelectTriggerResetType_Distance:
      {
        cWorkout.eTriggerType = triDistance;
        EEPROM_eTriggerType_Save(cWorkout.intIndex, cWorkout.eTriggerType);
        mnuDraw();
        FlashMessage(F("Trigger - Distance"));
        eSong = eSong_Confirm;
      }
      break;

    case   mnuAction_SelectTriggerResetType_TimeDelay:
      {
        cWorkout.eTriggerType = triTimer;
        EEPROM_eTriggerType_Save(cWorkout.intIndex, cWorkout.eTriggerType);
        mnuDraw();
        FlashMessage(F("Trigger - TImer"));
        eSong = eSong_Confirm;
      }
      break;

    case   mnuAction_TriggerDistance_SetMin:
      {
        cWorkout.bytTriggerDistance_Min = 
             EditValue("Trigger Dist. Min", cWorkout.bytTriggerDistance_Min, 2, 1023);
        EEPROM_bytTriggerDistance_Min_Save(cWorkout.intIndex, cWorkout.bytTriggerDistance_Min);
        mnuDraw();
        FlashMessage(F("Min dist set"));
      }
      break;

    case   mnuAction_TriggerDistance_SetResetTolerance:
      {
        cWorkout.bytTriggerDistance_Tolerance = 
             EditValue("Trigger toelrance", cWorkout.bytTriggerDistance_Min, 2, 1023);
        EEPROM_bytTriggerDistance_Tolerance_Save
                      (cWorkout.intIndex, cWorkout.bytTriggerDistance_Tolerance);
        mnuDraw();
        FlashMessage(F("Trigger Tolerance"));
      }
      break;

    case   mnuAction_TriggerTimerDelay_Set:
      {
        cWorkout.bytTriggerTimeStampReset_Delay = 
         (byte)EditValue("Reset delay", (int)cWorkout.bytTriggerTimeStampReset_Delay, 1, 255);
        EEPROM_bytTriggerTimeStampReset_Delay_Save
               (cWorkout.intIndex, cWorkout.bytTriggerTimeStampReset_Delay);
        mnuDraw();
        FlashMessage(F("Trig reset delay"));
      }
      break;

    case mnuAction_Workout_EditName:
      {
        cWorkout.Name = EditText(F("Edit Name:"), cWorkout.Name);
        EEPROM_Name_Save(cWorkout.intIndex, cWorkout.Name);
        mnuDraw();
        String strName = cWorkout.Name;
        strName.concat(F(" set"));
        FlashMessage(strName);
      }
      break;

    case mnuAction_Workout_EditNext:
      {
        Edit_Workout_Next();
        EEPROM_bytNextWorkout_Save(cWorkout.intIndex, cWorkout.bytNextWorkout);
        mnuDraw();
      }
      break;

    case   mnuAction_ViewCurrentWorkout:
      {
        ViewWorkout(&cWorkout);
      }
      break;

    case mnuAction_EEPROM_New:
      {
        cWorkout.Name = "Workout new";
        cWorkout.eWorkoutType = StopWatch_CountUp;
        cWorkout.eTriggerType = triTimer;
        cWorkout.bytTriggerDistance_Min = 20;
        cWorkout.bytTriggerDistance_Tolerance = 50;
        cWorkout.bytTriggerTimeStampReset_Delay = 125;
        cWorkout.bytTimer_Hours = 0;
        cWorkout.bytTimer_Minutes = 12;
        cWorkout.bytTimer_Seconds = 0;
        cWorkout.bytNextWorkout = 255;
        cWorkout.uintRepCounter = 50;
        cWorkout.intIndex = EEPROM_NumWorkouts_Get();
        EEPROM_NumWorkouts_Increment();
        EEPROM_Save(&cWorkout);
        mnuDraw();
        FlashMessage("Workout Reset");
        eSong = eSong_Challenge;
      }
      break;

    case mnuAction_EEPROM_Delete:
      {
        if (ConfirmEntry("Delete Workout?"))
        {
          EEPROM_Delete(&cWorkout);
          mnuDraw();
          FlashMessage(F(" - deleted -"));
          eSong = eSong_Confirm;
        }
      }
      break;

    case   mnuAction_EEPROM_Load:
      {
        cWorkout = EEPROM_Load();
        eSong = eSong_Confirm;
      }
      break;

    case mnuAction_Preferences_DefaultWorkout:
      {
        FlashMessage(F("Set Default"));
        int intSelectedIndex = EEPROM_WorkoutSelect();
        if (intSelectedIndex >= 0)
        {
          EEPROM_Preferences_DefaultWorkout_Save((byte)intSelectedIndex);
          mnuDraw();
          FlashMessage(F("default set"));
          eSong = eSong_Confirm;
        }
        else
        {
          mnuDraw();
          FlashMessage(F("set default aborted"));
          eSong = eSong_Cancel;
        }
      }
      break;

    case mnuAction_Preferences_ToggleSound_Workout:
      {
        bolPlaySound_Workout = !bolPlaySound_Workout;
        EEPROM_Preferences_PlaySound_Save(bolPlaySound_Workout);
        mnuDraw();
        FlashMessage(bolPlaySound_Workout ? F("WO Sound On") : F("WO Sound OFF"));
        eSong = eSong_Confirm;
      }
      break;

    case mnuAction_Preferences_ToggleSound_System:
      {
        bolPlaySound_System = !bolPlaySound_System;
        EEPROM_Preferences_PlaySound_Save(bolPlaySound_System);
        mnuDraw();
        FlashMessage(bolPlaySound_System ? F("Sys Sound On") : F("Sys Sound OFF"));
        eSong = eSong_Confirm;
      }
      break;

    case mnuAction_NoAction:
      cMnu_Current = cMnu_Selection;
      cMnu_Selection = cMnu_Current->cFirstChild;
      mnuDraw();
      break;
  }
}

and you can see, near the bottom of the code above, the last case listed is mnuAction_NoAction where the variables cMnu_Current & cMnu_Selection are set and the menu is drawn again according to the user's intentions.

User-Interface

update: 2020/09/25

It just occured to me that if you have your own Reps-Counter put together and running you may need some helping using the interface.

Since there are only two lines on the LCD the menus are very basic and there is no Clippy Image 20 to help you.  So I've put together a table describing the functions of each button depending on where you are in the menus.

Image 21

Buzzer Kind of Music

And then there's the buzzer. The Piezo buzzer is a ubiquitous descendant of the first electric buzzer which was invented in 1831 by Joseph Henry. Though the Piezoelectric buzzer sounds Italian, it was first manufactured by the Japanese in 1970s and 1980s. They are still found virtually everywhere. The one that's currently residing inside my Reps Counter was salvaged from a broken coffee maker I found on my way home from a nearby soup-kitchen. After gutting this plastic by-product of matinal indolence and the need to ingest dark brewed battery acid in a cup I found four push-buttons and one buzzer before neglecting the heating coil which would only have gotten me in trouble with the local fire department. The pleasant mocha-morning sounds that once woke up my neaghbours with a hot cup-a-jo now jog me from idleness in my daily workouts.

The week-long Arduino starter-course I took with Udemy some eight months ago shared a lecture on the piezo-buzzer which included a homework component that required that I write an Arduino project to play The Itzy Bitzy Spider song on the piezo-buzzer, which I did. My solution was a C# kind of solution and resorted to writing the song-stream into a string of text and cycle through the differing characters playing that song. That solution, unfortunately, needed to be modified in order for this current project not to crash on me for want of memory.

So I wrote essentially the same code again.

Ok, that was probably not the best solution but I did considerably cut down the amount of strings required. Where the frequency of the note used to be included in the song's string it is now encoded using a single character (from three characters to describe a value that could have fit inside a two byte integer variable instead of three characters it takes to write it in a string). And the note's length is also encoded into a numeric character that ranges from 0 to 4. So that means that I'm using up a whole byte of data where three bits would do. Clearly, there is room for improvement if memory becomes scarce again but since the project is working and it does all I want it to do the ease of writing songs into the project this way makes this memory trade-off a tolerable choice.

The way it's currently implemented, each song is written onto two strings of identical length equal to the number of notes in the song. These strings are then only referenced through a switch()-case that reads the strings it needs to play the current note of the current song. One global variable holds the index to the selected song, another for the index of the current note in that song and a third is an unsigned long integer which holds the results of a millis() call needed to know when the next note needs to be heard in order for the song to sound something like its supposed to.

I play no musical instrument. Can barely hum a tune. I just never really got into it and the punk-rock band I used to ~sing~ for when I was a teenager never got out of the garage. So that was the end of that. And so, to make the songs in the Rep-Counter sound better than Anne Frank in a tin klezmer band I downloaded some sheet music and deciphered it as best I could guess and got half-decent results using the images below to figure out which notes follow what. Whether the music I'm hearing from my piezo is a euphonious approximation of the composer's intended soothing melodies is a different matter altogether.

Here are some notes I used to decipher this mysterious musical cypher:

Image 22

The image below shows some kind of music staff (staves ?). As far as I can figure, the location of those black roundy things indicates the frequency at which the piezoelectric buzzer needs to play its tone. While the shape of those black roundy things tells the piezo how long to play those tones. Since there is only one character available to describe each of those two parameters, the image below labels each frequency with a specific code and the image above deciphers two different ways to indicate duration. There are two ways to tell time because music people need to keep time for both sound and silence. The sounds are called 'notes' and where their position in the staves below describe the frequency of that sound the shapes of the black roundy things (a.k.a. note) describe the duration of those sounds (or silence as the case may be). It turns out I could only figure four different time segments that music people use like a corrupted Babylonian measure of 16. Which happens to be better for the Arduino than any metric system anyway because the encoded 'time duration' values are powers of 2. Or bit locations in a byte. One sixteenth is 20 = 1 and divide it all by 16 and there's your 1/16th.

Image 23

As an example, you can have a look at the sheet music for Taps and see how the encrypted black roundy things' positions and shapes helped me break this mysterious musical code and decipher it all for the Arduino song strings to read without even filling out a single Freedom of Information Request form.

C#
// decoded music strings readable by Arduino
strFrequencies = F("ffCfCEgCEgCEgCCEGECgggC");
strDuration    = F("22322322222222223223223");

Image 24

These ciphers are stored in the function below:

C#
void playSong()
{
  if (eSong == eSong_Silence)
    return;

  switch (eSong)
  {
    case eSong_Cancel:
    case eSong_Confirm:
      if (!bolPlaySound_System) return;
      break;

    default:
      if (!bolPlaySound_Workout) return;
      break;
  }

  int intFrequencies[] =
  {// frequencies associated with characters sequenced in strNotes below
    220, // a
    247, // b
    261, // c
    293, // d
    330, // e
    349, // f
    392, // g
    440, // A
    493, // B
    523, // C
    587, // D
    659, // E
    698, // F
    783, // G
    880, // Á
    987, // ß
    1046, // Ç
    1174, // Ð
    1318, // Ë
    0 // space
  };

  String strNotes = F("abcdefgABCDEFGÁßÇÐË "); // index of note frequency 
                    // to be played found in this string is used to index intFrequencies[] 
  String strFrequencies = "";
  String strDuration    = "";

  switch (eSong)
  {

    case eSong_Taps:
      {
        intTempo = 30;
        /*
          strFrequencies = F("ffCfCEgCEgCEgCCEGECgggC");    // original sequence of note 
                                                            // frequencies as deciphered 
                                                            // from sheet music
          strDuration    = F("22322322222222223223223");    // durations
          /*/
        strFrequencies = F("CEGECgggC ");                   // abbreviated sequence of 
                                                            // note frequencies 
        strDuration    = F("2232232232");                   // durations
        //*/
      }
      break;

/*             REMAINING SONGS EXCISED FOR BREVITY               */

  }

  ulngMillis = millis();

  if ( ulngMillis > ulngMillis_OLD + intDuration )
  {
    char chrFreq = strFrequencies[intNoteCounter];  // find character of the current note 
                                                    // by its index in the 
                                                    // current song's string

    int intFreqIndex = strNotes.indexOf(chrFreq);   // find chrFreq in strNotes and 
                                                    // use index to determine which element 
                                                    // in intFrequencies[] array is 
                                                    // correct frequency
    if (intFreqIndex >= -0)
    {
      int intFrequency = intFrequencies[intFreqIndex];  // note frequency is found 
                                                        // using the char index of note 
                                                        // to be played as found in 
                                                        // strNotes above
      char chrDuration = strDuration [intNoteCounter];  // note duration is equal to the 
                                                        // char's numeric value as the 
                                                        // power of the value of 2 times 1/16th
      int intDurationIndex =  chrDuration - '0';
      intDuration = pow(2, intDurationIndex) * 
            (intTempo_Max - intTempo); // 2^n / 16 -> (16/16 = whole note), (8/16 half), 
                                       // (4/16 quarter), (2/16 eighth), (1/16 sixteenth)
      tone(pinBuzzer, intFrequency, intDuration);
    }

    ulngMillis_OLD = ulngMillis;

    intNoteCounter = (intNoteCounter + 1) % strFrequencies.length();
    if (intNoteCounter == 0)
    {
      noTone(pinBuzzer);
      eSong = eSong_Silence; //
    }
  }
}

And this is what they call music. To my ear, it sounds like a piezo buzzer making sweet melodies.

Sparing the Batteries

update: 2020/09/25

Since I've been using my Reps-Counter with a square 9V battery the need to replace the battery has become problematic.  Just yesterday I set out to do a 90 minute set and only 30 minutes into it I could barely read the LCD and had to count in my head, defeating the purpose of having the Reps-Counter in the first place.  The rechargeable batteries have finally arrived along with the USB charge-adaptors so I'll get on that soon but for now I've made a few minor software changes to help the batteries last longer.

Where the counter refreshed the screen after time the program flow went around its loop (several times per second) it now waits a delay period of

unsigned long ulngScreenRefresh_TimerDelay = 1000;  // milliseconds

before refreshing the screen.  This means there is no longer any sub-second precision in the display but the battery savings far outweigh that need for normal purposes.   I may include a Menu-Preferences option to allow the user to specify the Battery-Saving options but that will come down the road. 

The Reps-Counter also now refreshes the Reps-Count value on the screen only if it that value has actually changed.

of course, turning off the feed-back sounds will also reduce the strain on the battery.

I haven't had the opportunity to test these changes but I am confident that they will make a big difference as the batteries were dying far too quickly before, and these changes could not possibly have made it worse.

Gimme, Gimme, Gimme

If you would like an Exercise Reps Counter but have sworn a legally binding oath preventing you from playing with an Arduino and making your own, then just send me an email. There's a good chance that if you pay for the parts and shipping, I'll be happy to make you one because for anyone who takes exercise seriously, this thing rocks.

Final Comments

Nah... I will make no final comments for assuming anything is final is a presumption that the chasm of our collective ignorance will go unchanged. Beyond that, I saw the image of Jesus staring back at me from a hairy dog's ass on Facebook today... nuf sed.

History

  • 8th September, 2020: First published
  • 25th September, 2020 - code changes to alleviate strain on batteries & added more details about the Menu User-Interface
  • 28th September, 2020 - code changes - fixed end of workout Timer Display bug that crept in during last update
  • 29th September, 2020 - added note about how to reset EEPROM
  • 17th February, 2022 - updated Voltage Divider Resistance Calculator's Arduino code to handle debouncing issues
  • 14th March, 2022 - uploaded PlatformIO version of the Reps Counter code

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
CEO unemployable
Canada Canada
Christ Kennedy grew up in the suburbs of Montreal and is a bilingual Quebecois with a bachelor’s degree in computer engineering from McGill University. He is unemployable and currently living in Moncton, N.B. writing his next novel.

Comments and Discussions

 
QuestionMessage Closed Pin
21-Dec-20 16:42
Quỳnh Mạnh21-Dec-20 16:42 
QuestionGreat work! Pin
miroha14-Oct-20 8:21
professionalmiroha14-Oct-20 8:21 
AnswerRe: Great work! Pin
Christ Kennedy14-Oct-20 15:18
mvaChrist Kennedy14-Oct-20 15:18 
? what do you mean?
my code is perfect until i don't find a bug...

GeneralRe: Great work! Pin
miroha15-Oct-20 4:16
professionalmiroha15-Oct-20 4:16 
GeneralRe: Great work! Pin
Christ Kennedy16-Oct-20 9:24
mvaChrist Kennedy16-Oct-20 9:24 
Questiongreat work! I have a ton of ideas Pin
DRWASAKI16-Sep-20 11:37
DRWASAKI16-Sep-20 11:37 
AnswerRe: great work! I have a ton of ideas Pin
Christ Kennedy20-Sep-20 6:33
mvaChrist Kennedy20-Sep-20 6:33 
GeneralRe: great work! I have a ton of ideas Pin
DRWASAKI20-Sep-20 7:44
DRWASAKI20-Sep-20 7:44 
QuestionThank you Pin
PeterAlbronda8-Sep-20 23:43
PeterAlbronda8-Sep-20 23:43 
AnswerRe: Thank you Pin
Christ Kennedy9-Sep-20 7:54
mvaChrist Kennedy9-Sep-20 7:54 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.