One button, two behaviours, non-blocking code

I have configured my pushbuttons to do one of 3 things:

  1. If the holding time is longer than a threshold, immediately return value B, without waiting for release.
  2. If, upon release, the holding time turns out to have been shorter than the threshold, return value A.
  3. By default, if the button wasn't touched, or it was held for less than the debouncing threshold, return 0.

I'm happy with my function for the time being: in my current application, it doesn't matter that I have a blocking delay() and a blocking while loop. However, this is going to be an issue the day I want to add some functionality to my prototype. For a test, I added an LED that blinks at 20 Hz via a non-blocking state machine. Indeed, when I push a button, the blinking stops until I release the button.

I think I can manage to get rid of the delay(), but I'm having a hard time attempting to convert the while part into a non-blocking version that will give me the same behaviour as the one I have currently.

This is my original, working function:

int pollButton (struct Button* button)
{
    bool state = digitalRead(button->pin);
    button->pressDetected = false;

    if (button->state != state)
    {
        delay(DEBOUNCE);
        button->state = state;
        button->timestamp = millis();

        while (digitalRead(button->pin) == PRESSED)
        {
            button->pressDetected = true;

            if (millis() - button->timestamp > LONG_PRESS)
            {
                button->pressDetected = false;
                return 0 - button->weight;                
            }            
        }        
    }
    
    if (button->pressDetected)
        return button->weight;
    
    return 0;
    
} // end pollButton()

And this is my failed attempt at a non-blocking version (the buttons are completely unresponsive when I use it):

int pollButtonNB (struct Button* button)
/* Non-blocking version */
{
    bool state = digitalRead(button->pin);
    button->pressDetected = false;

    if (button->state != state)
    {
        button->timestamp = millis();

        if (millis() - button->timestamp > DEBOUNCE &&
            digitalRead(button->pin == PRESSED))
        {
            button->pressDetected = true;
            button->state = PRESSED;
            
             if (millis() - button->timestamp > LONG_PRESS)
             {
                button->timestamp     = millis();
                button->pressDetected = false;
                return 0 - button->weight;
             }
        }

        if (button->pressDetected && state == RELEASED)
        {
            button->pressDetected = false;
            return button->weight;
        }
    }
    return 0;
} //end pollButtonNB()

This is how I define a button:

struct Button 
{
    const uint8_t pin:7;
    const uint8_t weight:7;
    bool pressDetected:1;
    bool state:1;
    unsigned long timestamp;  
};

And this is how I invoke my function within the loop():

    for (size_t i = 0; i < NUMBER_OF_BUTTONS; ++i)
    {
        int x = pollButtonNB(aButtons[i]);
      //int x = pollButton(aButtons[i]); 
        
        if (x)
        {
            gCounter += x;
            if (gCounter < 0) gCounter = 0;
            gRefreshDisplay = true;
        }
    }

you only execute the timer when there is a button state change.

  • check for a state change
  • if pressed, capture a timestamp
  • if release, report short press
  • if the button is being pressed check if timeout exceeded and report long press

It looks like you are re-inventing the Bounce2 library. Have a look at how it is done within that code.

I think this will do what you want without blocking. You should be able to encapsulate this in an object and pass in callback functions for short-press and long-press.

const byte ButtonPin = 2;
const unsigned long DebounceTime = 30;
const unsigned long ButtonLongPressTime = 2000;

boolean ButtonWasPressed;  // Defaults to 'false'
boolean ButtonWasLongPressed;  // Defaults to 'false'
unsigned long ButtonStateChangeTime = 0; // Debounce/Long Press timer

void setup()
{
  pinMode (ButtonPin, INPUT_PULLUP);  // Button between Pin and Ground
}

void loop()
{
  unsigned long currentTime = millis();

  boolean buttonIsPressed = digitalRead(ButtonPin) == LOW;  // Active LOW

  // Check for button state change and do debounce
  if (buttonIsPressed != ButtonWasPressed &&
      currentTime - ButtonStateChangeTime > DebounceTime)
  {
    // Button state has changed
    ButtonStateChangeTime = currentTime;
    ButtonWasPressed = buttonIsPressed;

    if (ButtonWasPressed)
    {
      // Button was just pressed
      ButtonWasLongPressed = false;
    }
    else
    {
      // Button was just released
      if (!ButtonWasLongPressed)
      {
        // ACT ON SHORT PRESS
      }
    }

    // Check to see if we are in a long-press
    if (ButtonWasPressed && ! ButtonWasLongPressed &&
        currentTime - ButtonStateChangeTime > ButtonLongPressTime)
    {
      ButtonWasLongPressed = true;
      // ACT ON LONG PRESS
    }
  }
}

I've been messing about with my own button class and have things working like this:
KTS_Button_Example.ino - Wokwi Arduino Simulator Also added a blinking LED so you can check for non-blocking.

Thank you so much, your code does what I needed. I just had to adapt it to my naming scheme and add a member to my Button struct. I wasn't able to follow its logic at 100%, but at least, it made some sense!

Here is how my function and my new struct turned out to be, in case anyone is following this thread.

struct Button 
{
    const uint8_t pin:7;
    const uint8_t weight:7;
    bool pressDetected:1;
    bool longPressDetected:1; /*this is new*/
    bool state:1;
    unsigned long timestamp;  
};

int pollButtonNB (struct Button* button)
/* Non-blocking version */
{
    unsigned long currentTime = millis();
    bool buttonPressed = digitalRead(button->pin) == PRESSED;

    if (buttonPressed != button->pressDetected &&
        currentTime - button->timestamp > DEBOUNCE)
    {
        button->timestamp = currentTime;
        button->pressDetected = buttonPressed;

        if (button->pressDetected)
            // Button was just pressed
            button->longPressDetected = false;
            
        else if (! button->longPressDetected)
            // Button was just released
            return button->weight;
    }

    if (button->pressDetected && ! button->longPressDetected &&
        currentTime - button->timestamp > LONG_PRESS)
    {
        button->longPressDetected = true;
        return 0 - button->weight;
    }
    
    return 0;
    
} //end pollButtonNB()

Now that you have a 'struct' you are very close to having a 'class'. The only difference is that the first entries in a 'struct' are public and the first entries in a 'class' are private. Start by moving the functions into the struct:

struct Button
{
  Button(int pin, int weight) : pin(pin), weight(weight) {}
  void begin() {pinMode(pin, INPUT_PULLUP);}
  const uint8_t pin: 7;
    const uint8_t weight: 7;
    bool pressDetected: 1;
    bool longPressDetected: 1; /*this is new*/
    bool state: 1;
    unsigned long timestamp;
    int poll();
    const int PRESSED = LOW;
    const unsigned DEBOUNCE = 20;
    const unsigned LONG_PRESS = 2000;
  } WeightButton(2, 12);  // Declare an instance of Button

// Now that 'poll' is a member of the Button struct, you 
// don't have to pass a pointer to the struct.
  int Button::poll() /* Non-blocking version */
{
  unsigned long currentTime = millis();
  bool buttonPressed = digitalRead(pin) == PRESSED;

  if (buttonPressed != pressDetected &&
      currentTime - timestamp > DEBOUNCE)
  {
    timestamp = currentTime;
    pressDetected = buttonPressed;

    if (pressDetected)
    {
      // Button was just pressed
      longPressDetected = false;
    }
    else
    {
      // Button was just released
      if (! longPressDetected)  // Short press?
        return weight;
    }

  if (pressDetected && ! longPressDetected &&
      currentTime - timestamp > LONG_PRESS)
  {
    longPressDetected = true;
    return 0 - weight;
  }

  return 0;
}

void setup()
{
  WeightButton.begin();
}

void loop()
{
  int wb_val = WeightButton.poll();
}

Now all you need to do to make it a 'class' is change:

struct Button
{

to

class Button
{
  public: