Button press not always detected by ezButton

I have a fairly big project and have run into a strange situation. I am asking for general design guidance, not so much detailed code debugging. I have ~2,800 lines of code spread across multiple ESP32S3s and 20+ files (.h and .ino). I just want to make sure my code layout is sound.

In general, all the code functions as expected. The boards talk to each other, data is stored and retrieved in Preferences, and the states are executed as expected. But I ran into one weird situation. I have a rotary encoder with a switch on one of the boards. All the code runs in a SimpleFSM state machine design and ezButton for buttons.

On the controller board (the one with the rotary encoder), I have this code layout:

sketch_controller
β”œβ”€β”€ commands.h
β”œβ”€β”€ controller_esp_now.h
β”œβ”€β”€ controller_esp_now.ino
β”œβ”€β”€ controller_fsm.h
β”œβ”€β”€ controller_fsm.ino
β”œβ”€β”€ debug.cfg
β”œβ”€β”€ launcher_storage.h
β”œβ”€β”€ launcher_storage.ino
β”œβ”€β”€ libraries
β”‚ └── ezButton
β”œβ”€β”€ rotary.h
β”œβ”€β”€ rotary.ino
β”œβ”€β”€ seven_segment.h
β”œβ”€β”€ seven_segment.ino
└── sketch_controller.ino

sketch_controller.ino is the main entry point. It looks like:

#include <ezButton.h>
#include <Preferences.h>
#include <stdbool.h>
#include <esp_now.h>
#include "commands.h"
#include "launcher_storage.h"
#include "rotary.h"
#include "seven_segment.h"
#include "controller_esp_now.h"
#include "controller_fsm.h"

/* hardware pin definitions */

void setup() {
/* setup serial, hardware buttons, */
storage_setup();
rotary_setup();
seven_segment_setup();
controller_esp_now_setup();
controller_fsm_setup();
}

void loop() {
/* enable some hardware ezButton loops */
rotary_loop();
seven_segment_loop();
controller_esp_now_loop();
controller_fsm_loop();
}

Each .ino has void <name>_setup()and void name_loop() methods.

My problem is the button on the rotary encoder. When I have:

rotary.h

#define ROTARY_BUTTON 14
ezButton select_launcher_btn(ROTARY_BUTTON);

rotary.ino

void rotary_setup() {
select_launcher_btn.setDebounceTime(50); // set debounce time to 50 milliseconds
}
void rotary_loop() {
select_launcher_btn.loop();
if(select_launcher_btn.isPressed()) {
Serial.println("The launcher_selected button is pressed");
}
}

The button press is not always detected. In some states of the FSM it is detected, but not others. If I move this code into controller.ino file, the rotary encoder button press is recognized every time. Note, I just moved the button definition and ezButton loop to the controller.ino, not the code that detects the button press.

#define ROTARY_BUTTON 14
ezButton select_launcher_btn(ROTARY_BUTTON);
void loop() {
select_launcher_btn.loop();
}

I tried putting a print statement in the rotary_loop() function, and it was always being called, as I expected, so I do not understand why the button is not detected. I also printed out the state of the button in the rotary_loop() function, and it printed 1 all the time even when the button was pushed (i.e. connected to ground).

Again, I am just looking to remove any potential issues with my overall design that may be causing this problem.

Thanks!

Since we can't see your code I would not use multiple .ino files, I would use .cpp.
I can't tell if you are using delay (BAD idea) and if you have a tight main loop where hopefully the button code is.
Maybe check the tutorials on the forum re state machine and doing multiple things at once.

Regarding the .ino files, they all have the same structure:

void <name>_setup() {
}
void <name>_loop() {
}

I don’t have any delays in the <name>_loop() functions. The only delays are in the setup() for the Serial setup and in the esp-now setup() as recommended by Espressif code examples.

In particular, there are no delays in the rotary_loop() where the button code fails.

I have never used .cpp files, always just .ino to separate my code into modules to make it easier to work on. Is it just a matter of changing the file extension, or do I have to refactor all the code to be compatible with .cpp files?

The differences are subtle. for an all ino setup the files are concatenated (I think in alpha order) and re-compiled. For cpp files only changed files are re-compiled saving time each edit.
I have not looked at your code in depth but I will take a copy and make those changes (I think one more change) to make sure it works.
I think the other change is a single .h file for everything.
Please post your code.

I can't tell you the reason, but this tells me that the button.loop() call is not as frequent in rotary.ino as controller.ino.

Apart from setup, can you confirm you have no delays anywhere?

For and while loops also cause problems unless care is taken to ensure they complete quickly.

A lot of library code is, unfortunately, blocking; you should test how long each call to a library function takes to return.

Edit,
Change the state of a spare pin every time the button library is called, monitor the pin with an oscilloscope, that will make it very obvious if that's where the problem is.

I would agree with you, except I put a Serial.printf(β€œrotary_loop count=%d\n”, loop_counter); in the rotary_loop, and it produced this output after 30 seconds. Note, the loop_counter started at 0:

In rotary loop count=962686170
In rotary loop count=962686171
In rotary loop count=962686172
In rotary loop count=962686173
In rotary loop count=962686174
In rotary loop count=962686175
In rotary loop count=962686176
In rotary loop count=962686177
In rotary loop count=962686178
In rotary loop count=962686179
In rotary loop count=962686180
In rotary loop count=962686181
In rotary loop count=962686182
In rotary loop count=962686183
In rotary loop count=962686184
In rotary loop count=962686185
In rotary loop count=962686186

The printout was running down the screen. I don’t think there were any delays in the rotary_loop(). But I can’t explain the lack of button detection.

Edit

I change the loop counter to millis(). I don’t have my β€˜scope with me.

In rotary loop count=304650
In rotary loop count=304650
In rotary loop count=304651
In rotary loop count=304651
In rotary loop count=304661
In rotary loop count=304661
In rotary loop count=304661
In rotary loop count=304661
In rotary loop count=304671
In rotary loop count=304671
In rotary loop count=304671
In rotary loop count=304671
In rotary loop count=304682
In rotary loop count=304682
In rotary loop count=304682
In rotary loop count=304682
In rotary loop count=304682
In rotary loop count=304692
In rotary loop count=304692
In rotary loop count=304692
In rotary loop count=304692
In rotary loop count=304703
In rotary loop count=304703
In rotary loop count=304703

I looks like the rotary_loop() is being called roughly every 0.1 ms? Should be fast enough to detect a button push.

It's not the number of calls in a period of time that matters, it's the interval between them. If they are, for example, every millisecond but occasionally 100 milliseconds then you will have a problem.

millis() or micros()?

Good catch. It was millis().

Serial.printf("In rotary loop count=%d\n", millis());

I changed it to micros():

Serial.printf("In rotary loop count=%d\n", micros());

and got this output.

12:41:03.760 -> In rotary loop count=98649468
12:41:03.760 -> In rotary loop count=98649519
12:41:03.760 -> In rotary loop count=98649563
12:41:03.792 -> In rotary loop count=98649607
12:41:03.792 -> In rotary loop count=98659883
12:41:03.792 -> In rotary loop count=98659934
12:41:03.792 -> In rotary loop count=98659979
12:41:03.792 -> In rotary loop count=98660023
12:41:03.792 -> In rotary loop count=98670299
12:41:03.792 -> In rotary loop count=98670350
12:41:03.792 -> In rotary loop count=98670394
12:41:03.792 -> In rotary loop count=98670438
12:41:03.792 -> In rotary loop count=98680715
12:41:03.792 -> In rotary loop count=98680766
12:41:03.792 -> In rotary loop count=98680810
12:41:03.792 -> In rotary loop count=98680854
12:41:03.825 -> In rotary loop count=98691131
12:41:03.825 -> In rotary loop count=98691182
12:41:03.825 -> In rotary loop count=98691226
12:41:03.825 -> In rotary loop count=98691270
12:41:03.825 -> In rotary loop count=98701546
12:41:03.825 -> In rotary loop count=98701597
12:41:03.825 -> In rotary loop count=98701646

Looks like the loop executes every ~50 microseconds.

Which are the most problematic states?

I did some more checking, and it only gets weirder. Here are the two files, rotary.h and rotary.ino

rotary.h

#ifndef _Rotary_H
#define _Rotary_H
// Define rotary encoder pins

#define ENC_A 13
#define ENC_B 12
#define ROTARY_BUTTON 14

ezButton select_launcher_btn(ROTARY_BUTTON);
volatile int counter = 100;
static int lastCounter = 10;
int limited_counter = 5;
int SELECTED_LAUNCHER = 0;

void rotary_setup();
void rotary_loop();

#endif

rotary.ino

#include <stdbool.h>
#include "rotary.h"

void rotary_setup() {
  Serial.println("rotary_setup");
  // Set encoder pins and attach interrupts
  pinMode(ENC_A, INPUT_PULLUP);
  pinMode(ENC_B, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(ENC_A), read_encoder, CHANGE);
  attachInterrupt(digitalPinToInterrupt(ENC_B), read_encoder, CHANGE);

  select_launcher_btn.setDebounceTime(50); // set debounce time to 50 milliseconds
}

void rotary_loop() {
  select_launcher_btn.loop();

   //Serial.printf("In rotary loop count=%d\n", micros());

  // If count has changed print the new value to serial
  if(counter != lastCounter) {
    limited_counter = limit_value(9, 1, true);
    Serial.printf("counter=%d, limited_counter=%d\n", counter, limited_counter);
    //Serial.printf("fsm.getState()=%d", fsm.getState()->getID());
    counter = limited_counter;
    lastCounter = counter;
  }
  
  if(select_launcher_btn.isPressed()) {
    Serial.println("The launcher_selected button is pressed");
    //selected_count = limited_counter;
    SELECTED_LAUNCHER = limited_counter;
    Serial.printf("selected count=%d\n", SELECTED_LAUNCHER);
    fsm.trigger(select_launcher);
  }

  if(select_launcher_btn.isReleased()) {
    Serial.println("The launcher_selected button is released");
    //LAUNCHER_SELECTED = false;
  }

  if (fsm.isInState(&s[2])) {
    //print_valid_launchers();
    if (valid_launchers[limited_counter] == 1) {
      digitalWrite(DECIMAL_POINT, HIGH);
    }
    else {
      digitalWrite(DECIMAL_POINT, LOW);
    }
  }
}

The line Serial.printf("selected count=%d\n", SELECTED_LAUNCHER); always prints the correct value, regardless of the state of the overall state machine.

The line Serial.println("The launcher_selected button is pressed"); does not print in certain states of the state machine. Same with the corresponding released line.

The code starting if (fsm.isInState(&s[2])) also works in all states - the decimal point is illuminated at all the right times.

To add a humorous side note. The only state where the rotary encoder's button performs a useful function is the one state it does not work. All the other states where the button works, it doesn't matter to the overall functioning of the project. Talk about Murphy/Mayhem at his best!

I am stumped as how to debug this problem.

Yes, very weird that the string literals are the issue rather than the printf() constructions.

What happens if you change the problematic statements to

printf("The launcher selected button is %s", "pressed\n");
printf("The launcher selected button is %s", "released\n");

I am stumped as how to debug this problem.

I have ~2,800 lines of code spread across multiple ESP32S3s and 20+ files (.h and .ino).

It's not what you want to hear, but if you can start with a chunk of working code which reads the encoder and prints the state of the button you can then start adding files until it breaks.

My apologies for being unclear. I was just using the print statements to show which sections of code were or were not being executed. The actual print statements are irrelevant. However, I tried your suggestion, and changing println to printf in the rotary_encoder.isPressed() block makes no difference.

The issue is the rotary_button.isPressed() statement, when in the rotary_loop() block, is not being executed in certain states, whereas the rotary encoder values are being processed in all states in the same rotary_loop() block. However, if I move the rotary_button.isPressed() to the entry point controller.ino loop() block, then the rotary button being pressed is recognized in all states.

I can debug this problem as you suggest, or I can just put the rotary_button.isPressed() in the controller.ino loop() block and continue with the design/implementation. I was hoping there would be a more streamlined approach to debugging this issue without starting over with a blank .ino file.

My main goal was to get feedback on my overall design approach, using multiple .ino and .h files to separate my code into manageable and understandable chunks, instead of one giant .ino file with literally thousands of lines of code in setup() and loop(). No one has come back to say that this approach does not work for these reasons, nor to say the reason the button does not work is how I have structured my code base.

I put this simple "heartbeat" code as the first line of loop() in my code when I need to "see" the function at work.

  digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN)); // configue pinMode(LED_BUILTIN, OUTPUT);

I was surprised to see a very irregular heartbeat in some of my sketches. Seeing this irregularity let me know I had some work to do.

1 Like

Great idea.

I tried something similar by putting

Serial.printf("In main loop usec=%d\n", micros());

at the top of the main loop and got the following results.

It looks like there is a 10 msec delay in the main loop. That still would not explain why the button press is not detected.

		            microsec	difference
In main loop usec	68147436	
In main loop usec	68157692	10,256
In main loop usec	68157745	53
In main loop usec	68157798	53
In main loop usec	68157851	53
In main loop usec	68157904	53
In main loop usec	68168195	10,291
In main loop usec	68168248	53
In main loop usec	68168301	53
In main loop usec	68168354	53
In main loop usec	68178610	10,256
In main loop usec	68178668	58
In main loop usec	68178720	52
In main loop usec	68178773	53
In main loop usec	68178826	53
In main loop usec	68189113	10,287
In main loop usec	68189167	54
In main loop usec	68189219	52
In main loop usec	68189272	53
In main loop usec	68199528	10,256
In main loop usec	68199581	53
In main loop usec	68199639	58
In main loop usec	68199692	53
In main loop usec	68199745	53
In main loop usec	68210032	10,287
In main loop usec	68210085	53
In main loop usec	68210138	53
In main loop usec	68210191	53
In main loop usec	68220446	10,255
In main loop usec	68220499	53
In main loop usec	68220552	53
In main loop usec	68220605	53
In main loop usec	68230862	10,257
In main loop usec	68230914	52
In main loop usec	68230967	53
In main loop usec	68231020	53
In main loop usec	68231073	53
In main loop usec	68241365	10,292
In main loop usec	68241418	53
In main loop usec	68241471	53
In main loop usec	68241523	52
In main loop usec	68251780	10,257
In main loop usec	68251833	53
In main loop usec	68251885	52
In main loop usec	68251938	53
In main loop usec	68251991	53
In main loop usec	68262283	10,292
In main loop usec	68262336	53
In main loop usec	68262389	53
In main loop usec	68262442	53

Be aware that serial prints might affect the timing. I'm not familiar with ESP32 based boards but on AVR based boards the print statements write to buffer which is emptied in the background (interrupts); if that buffer is full the program will wait till there is space to add a new character. You're probably better of using a logic analyser or scope (if you have one of them).

I just scanned this thread, so sry if this has been iterated, I reiterate.

ezButton once got my highest praise for button libraries "it doesn't suck". But it has a real flaw.

If you do not call the service method for ezButton frequently enough, it does not work very well.

Call ezButton at least twice as often as the debounce constant you set.

Also (not at my big rig) I think ezButton requires a user to make the call to set the debounce time interval. Use the setDebounceTime method - the default is 0.

a7

There seem to be two ezbutton libraries, EZButton (GitHub - IPdotSetAF/EZButton: Arduino library that transforms raw button/touch inputs into events easily.) and ezButton (GitHub - ArduinoGetStarted/button: Button library supports debounce, pressed/released events. It is easy to use with multiple buttons. It is designed for not only beginners but also experienced users); note the capitalisation.

The latter has the problem that it's lacking a begin() method and sets the pinMode in the constructor; depending on the processor that might bite you because that initialisation is done before main() is called and as a result the initialisation might be reset; it's OK on AVR but there is a board where that causes problems, can't remember for sure which one.

I've never looked at the first one.

That 10ms is blocking all inputs for 200 times the length of the standard loop. Find it.

My guess is you have a "tiny" loop to make 10 readings that happens 20 times in the 200ms between "10,0000ms" readings. The pattern seems to be "200."