Two Buttons: Click, double-click each and hold both

I'm working on a toy that will have a bunch of inputs. Right now I'm testing it out with buttons. What I'd like to do is detect the following:

Button 1:
Single click - Event A
Double click - Event B

Button 2:
Single click - Event C
Double click - Event D

Hold both buttons for 2 seconds: Event E

I've been looking at various libraries and examples and found a really nice one linked here on the forum:

It's very well documented and uses a fast, efficient debouncing method.

The author had been very helpful in answering questions, but when I looked to see what he'd done recently, there was nothing - and no updates on his Github either. Sadly, a Google search informed me he'd passed away from cancer a few years ago.

Sorry to bring that news.

Anyway, I can get buttons 1 and 2 to detect single and double clicks on their own, but I haven't been able to figure out how to get the hold feature working with them. Here is the code so far:

#define KEY1_PIN A2
#define KEY2_PIN A3

#define LEDA_PIN 8
#define LEDB_PIN 13
#define LEDC_PIN 10

#define BLINK_TIME 300
#define REACTION_TIME 500

#include "SwitchPack.h"
DoubleClick key1(KEY1_PIN, PULLUP, REACTION_TIME);
DoubleClick key2(KEY2_PIN, PULLUP, REACTION_TIME);

//setup==========
void setup() {
  key1.begin();
  key1.setSensitivity(16);
  key2.begin();
  key1.setSensitivity(16);

  pinMode(LEDA_PIN, OUTPUT);
  pinMode(LEDB_PIN, OUTPUT);
  pinMode(LEDC_PIN, OUTPUT);
}

//loop=======================
void loop() {
  int event;

  event = key1.clickCount();

// This section causes issues
  if (key1.closed() && key2.closed())  {
    event = 3;}

  switch (event) {
    case 1:
      clickEvent1();
      break;
    case 2:
      doubleClickEvent1();
      break;
    case 3:
      doubleHoldEvent();
      break;
  }
}

//=================================================
// Events to trigger

void clickEvent1() {
  (digitalWrite(LEDA_PIN, HIGH));
  delay(250);
  digitalWrite(LEDA_PIN, LOW);
}
void doubleClickEvent1() {
  digitalWrite(LEDB_PIN, HIGH);
  delay(250);
  digitalWrite(LEDB_PIN, LOW);
}
void doubleHoldEvent() {
  (digitalWrite(LEDC_PIN, HIGH));
  delay(250);
  digitalWrite(LEDC_PIN, LOW);
}

Where I mentioned "This section causes issues" if I un-comment it, the code will sometimes interpret a click incorrectly - a double click instead of a single.

Are you supposed to set the sensitivity of key2 somewhere?

Thanks, good catch. Fixed, but it doesn't change anything.

Looking at the examples it looks like if you want you use the closed() method you need to first define each button as...

Contact key1contact(KEY1_PIN, PULLUP);
Contact key2contact(KEY2_PIN, PULLUP);

Names will need to be different to the one you''ve already used for the single/double click stuff.

Then check
if (key1contact.closed() && key2contact.closed())

1 Like

Thanks, that improves it. I thought that since the later classes inherited the characteristics of the earlier ones that I didn't need to define things that way.

Here's the updated code. Now, after doing the double hold and getting that event, I get a singleClick event when I release both buttons.

I'm trying to zero it out so I don't get another event until another button is clicked. but can't find the right spot.

/*
 * DoubleClick.ino
 * Author: Jacques Bellavance
 * Date : August 9, 2017
 * Released under the GNU General Public License v3.0
 * 
 * Demonstrates how to use the DoubleClick class
 * The .clickCount() method returns the number of times that
 * the switch has been clicked during a specified period
 * 0 = no click
 * 1 = 1 click
 * 2 = 2 clicks
 * 
 * We will have half a second (500) milliseconds 
 * to perform a single or a double-click
 * 
 */

#define KEY1_PIN A2
#define KEY2_PIN A3

#define LEDA_PIN 8
#define LEDB_PIN 13
#define LEDC_PIN 10

#define BLINK_TIME 300
#define REACTION_TIME 500

#include "SwitchPack.h"
DoubleClick key1(KEY1_PIN, PULLUP, REACTION_TIME);
DoubleClick key2(KEY2_PIN, PULLUP, REACTION_TIME);
Contact key1contact(KEY1_PIN, PULLUP);
Contact key2contact(KEY2_PIN, PULLUP);

//setup==========
void setup() {
  key1.begin();
  key1.setSensitivity(18);
  key2.begin();
  key2.setSensitivity(18);
  key1contact.begin();
  key2contact.begin();

  pinMode(LEDA_PIN, OUTPUT);
  pinMode(LEDB_PIN, OUTPUT);
  pinMode(LEDC_PIN, OUTPUT);
}

//loop=======================
void loop() {
  int event;

  event = key1.clickCount();

  if (key1contact.closed() && key2contact.closed()) {
    event = 3;
  }


  switch (event) {
    case 1:
      clickEvent1();
      break;
    case 2:
      doubleClickEvent1();
      break;
    case 3:
      doubleHoldEvent();
      break;
  }
  if (event == 3) {   // Doesn't reset event counter
    event = 0;
  }
}

//=================================================
// Events to trigger

void clickEvent1() {
  (digitalWrite(LEDA_PIN, HIGH));
  delay(250);
  digitalWrite(LEDA_PIN, LOW);
}
void doubleClickEvent1() {
  digitalWrite(LEDB_PIN, HIGH);
  delay(250);
  digitalWrite(LEDB_PIN, LOW);
}
void doubleHoldEvent() {
  (digitalWrite(LEDC_PIN, HIGH));
  delay(250);
  digitalWrite(LEDC_PIN, LOW);
}

Without understanding the details of that library, it's a bit hard to say what your options are.

Have you considered writing your own code for the button handling? None of it is very difficult and you would have full control over how the functionality works.

I think you're right, I looked at a lot of libraries and none of them handle things the way I need. They deal with single switches well, and sometimes multiple switches, but never switches in combination.

Thinking about this during the day, the key seems to be identifying the double-hold and then ignoring the other events until the hold is released.

Pseudocode...

unsigned int longHoldStart;
int longHoldInterval = 2000;


If buttonA.isClosed AND buttonB.isClosed {
  longHoldStart = millis();
  buttonA.clickCount = 0;
  buttonB.clickCount = 0;
}

  If millis() - longHoldStart >= longHoldInterval {
  event = 4;
}

How does that look?

If you wrote your own code I think it would be somewhat simpler if you didn't use double clicks.

Would the following give you the required functionality you are looking for?

  • short click (either button)
  • long click (either button, but not both)
  • long click (both buttons)

That sounds good. In the interests of time, I can go with it, and then revisit it later for an upgrade. And I'll learn something.

What do you suggest as the next step?

Here's some code to get you started...

struct button
{
  int pin;
  int value;
  int previous;
  unsigned long onTime;
  unsigned long debounceStart;
  char pressType;
};

button button1 = {A1, HIGH, HIGH, 0, 0, ' '};           // Define a button.

unsigned long currMillis;

const unsigned long LONG_PRESS  = 1000;                 // Set as required.
const unsigned long DEBOUNCE    = 100;                  // Set as required.

void setup()
{
  Serial.begin(115200);
  pinMode(button1.pin, INPUT_PULLUP);
}

void loop()
{
  currMillis = millis();
  
  button1.value = digitalRead(button1.pin);

  if (currMillis - button1.debounceStart > DEBOUNCE)    // Don't do anything within the debounce time.
  {
    if (button1.value == LOW)                           // Is the button pressed?
    {
      if (button1.previous == HIGH)                     // Has it just been pressed?
      {
        button1.onTime        = currMillis;             // Reset the time the button was first pressed.
        button1.debounceStart = currMillis;             // Reset the debounce timer.
      }
    }
    else                                                // Button must be released.
    {
      if (button1.previous == LOW)                      // Has it just been released?
      {
        if (currMillis - button1.onTime > LONG_PRESS)   // How long was it pressed?
          button1.pressType = 'L';
        else
          button1.pressType = 'S';

        button1.debounceStart = currMillis;             // Reset the debounce timer.
      }
    }


 
    if (button1.pressType != ' ')                       // Check button pressType here...
    {
      Serial.print(button1.pressType);
      Serial.print(" ");
      Serial.println(currMillis - button1.onTime);
      button1.pressType = ' ';
    }


    button1.previous = button1.value;       
  }

  // Other code here...
}

See if you can figure out what it's doing... currently just a single button looking for a short or long button press. Presses are registered once the button is released.

Adding a second button should be pretty straightforward. Ideally you'd create a class for all this, but not essential at this stage.

When you get to checking if both buttons are held down (long press), then you will need to consider the lag between the 2 buttons... they won't be pressed/released at exactly the same time... so you'll need to come up with some logic to handle this.

For example... if button1 is held for >1s (long press), but button 2 has only been down for 900ms, what should happen? Is close enough Ok? This is probably the trickiest thing to think about.

Wow, thank you for all this! It's very generous.

With the holidays coming up, I probably won't get a chance to dig into this until next week. Happy Holidays!

@Jeff_Haas no worries mate... happy holidays to you too. Hit me up if you get stuck.

Finally back to this after the holidays. And the power is back on! Hooray!

I've gone through this, added a second button and put them into their own (inefficient) function.

It's pretty straightforward to add an extra button, code below.


struct button {
  int pin;
  int value;
  int previous;
  unsigned long onTime;
  unsigned long debounceStart;
  char pressType;
};

button button1 = { A2, HIGH, HIGH, 0, 0, ' ' };  
button button2 = { A3, HIGH, HIGH, 0, 0, ' ' };  

unsigned long currMillis;

const unsigned long LONG_PRESS = 800;  // Set as required.
const unsigned long DEBOUNCE = 100;    // Set as required.

void setup() {
  Serial.begin(9600);
  pinMode(button1.pin, INPUT_PULLUP);
  pinMode(button2.pin, INPUT_PULLUP);
  Serial.println("Ready");
}

void loop() {
  currMillis = millis();

  button1.value = digitalRead(button1.pin);
  button2.value = digitalRead(button2.pin);

  buttonChecker();
}

void buttonChecker() {
  if (currMillis - button1.debounceStart > DEBOUNCE)  // Don't do anything within the debounce time.
  {
    if (button1.value == LOW)  // Is the button pressed?
    {
      if (button1.previous == HIGH)  // Has it just been pressed?
      {
        button1.onTime = currMillis;         // Reset the time the button was first pressed.
        button1.debounceStart = currMillis;  // Reset the debounce timer.
      }
    } else  // Button must be released.
    {
      if (button1.previous == LOW)  // Has it just been released?
      {
        if (currMillis - button1.onTime > LONG_PRESS)  // How long was it pressed?
          button1.pressType = 'L';
        else
          button1.pressType = 'S';

        button1.debounceStart = currMillis;  // Reset the debounce timer.
      }
    }

    if (button1.pressType != ' ')  // Check button pressType here...
    {
      Serial.print("Button 1: ");
      Serial.print(button1.pressType);
      Serial.print(" ");
      Serial.println(currMillis - button1.onTime);
      button1.pressType = ' ';
    }

    button1.previous = button1.value;
  }

  ///// button 2

  if (currMillis - button2.debounceStart > DEBOUNCE)  // Don't do anything within the debounce time.
  {
    if (button2.value == LOW)  // Is the button pressed?
    {
      if (button2.previous == HIGH)  // Has it just been pressed?
      {
        button2.onTime = currMillis;         // Reset the time the button was first pressed.
        button2.debounceStart = currMillis;  // Reset the debounce timer.
      }
    } else  // Button must be released.
    {
      if (button2.previous == LOW)  // Has it just been released?
      {
        if (currMillis - button2.onTime > LONG_PRESS)  // How long was it pressed?
          button2.pressType = 'L';
        else
          button2.pressType = 'S';

        button2.debounceStart = currMillis;  // Reset the debounce timer.
      }
    }

    if (button2.pressType != ' ')  // Check button pressType here...
    {
      Serial.print("Button 2: ");
      Serial.print(button2.pressType);
      Serial.print(" ");
      Serial.println(currMillis - button2.onTime);
      button2.pressType = ' ';
    }

    button2.previous = button2.value;
  }

}

Thinking about the logic for the double hold, here's pseudocode for what I think could work. The intent here is to have the user hold both buttons until the event triggers. Then when it triggers, the user will release the buttons.

const unsigned long doubleHoldTime = 2000

if Button_1 Is_Pressed AND Button_2 Is_Pressed then start doubleHoldElapsed timer.  

if doubleHoldElapsed > than doubleHoldTime then (trigger event)
else (no event)

Need to figure out how to cancel/ignore responses from single buttons in the event both buttons are pressed. Perhaps clearing the timers for both single holds if a double hold event is detected?

Thoughts?

I think that would work. As you have figured out already the trick is to make sure that when the buttons are released this doesn't also trigger one of the other button outputs (B1.S, B1.L, B2.S, B2.L)... as these get determined when the button is released. You could maybe just add another boolean variable to the struct... called doublePress that gets set when the 2 buttons are held for >2 seconds. Then check this when the button is released, and if set do nothing (except reset doublePress)

Almost there...

Current code below detects when both buttons are pressed for 3 seconds, but I can't figure out how to reset the elapsed timer for both buttons. So the next press of both buttons triggers it immediately.


struct button {
  int pin;
  int value;
  int previous;
  unsigned long onTime;
  unsigned long debounceStart;
  char pressType;
  unsigned long doubleHoldElapsed;
  bool doublePress;
};

button button1 = { A0, HIGH, HIGH, 0, 0, ' ' };  // Define a button.
button button2 = { A1, HIGH, HIGH, 0, 0, ' ' };  // Define a button.

unsigned long currMillis;

const unsigned long LONG_PRESS = 800;  // Set as required.
const unsigned long DEBOUNCE = 100;    // Set as required.
const unsigned long doubleHoldTime = 3000;

const int LED1 = 8;
const int LED2 = 10;
const int LED3 = 12;

const byte Is_Off = 0;
const byte Is_On = 1;

void setup() {
  //miniLED board on pins 8 - 13
  for (byte j = 8; j < 13; j++) (pinMode(j, OUTPUT));

  pinMode(button1.pin, INPUT_PULLUP);
  pinMode(button2.pin, INPUT_PULLUP);

  Serial.begin(9600);
  Serial.println("Ready");
}

void loop() {
  currMillis = millis();

  button1.value = digitalRead(button1.pin);
  button2.value = digitalRead(button2.pin);

  buttonChecker();
}

void buttonChecker() {

  ///// button 1
  // if (currMillis - button1.debounceStart > DEBOUNCE)  // Don't do anything within the debounce time.
  // {
  //   if (button1.value == LOW)  // Is the button pressed?
  //   {
  //     if (button1.previous == HIGH)  // Has it just been pressed?
  //     {
  //       button1.onTime = currMillis;         // Reset the time the button was first pressed.
  //       button1.debounceStart = currMillis;  // Reset the debounce timer.
  //     }
  //   } else  // Button must be released.
  //   {
  //     if (button1.previous == LOW)  // Has it just been released?
  //     {
  //       if (currMillis - button1.onTime > LONG_PRESS)  // How long was it pressed?
  //         button1.pressType = 'L';
  //       else
  //         button1.pressType = 'S';

  //       button1.debounceStart = currMillis;  // Reset the debounce timer.
  //     }
  //   }

  //   if (button1.pressType != ' ')  // Check button pressType here...
  //   {
  //     Serial.print("Button 1: ");
  //     Serial.print(button1.pressType);
  //     Serial.print(" ");
  //     Serial.println(currMillis - button1.onTime);
  //     button1.pressType = ' ';
  //   }

  //   button1.previous = button1.value;
  // }

  ///// button 2

  // if (currMillis - button2.debounceStart > DEBOUNCE)  // Don't do anything within the debounce time.
  // {
  //   if (button2.value == LOW)  // Is the button pressed?
  //   {
  //     if (button2.previous == HIGH)  // Has it just been pressed?
  //     {
  //       button2.onTime = currMillis;         // Reset the time the button was first pressed.
  //       button2.debounceStart = currMillis;  // Reset the debounce timer.
  //     }
  //   } else  // Button must be released.
  //   {
  //     if (button2.previous == LOW)  // Has it just been released?
  //     {
  //       if (currMillis - button2.onTime > LONG_PRESS)  // How long was it pressed?
  //         button2.pressType = 'L';
  //       else
  //         button2.pressType = 'S';

  //       button2.debounceStart = currMillis;  // Reset the debounce timer.
  //     }
  //   }

  //   if (button2.pressType != ' ')  // Check button pressType here...
  //   {
  //     Serial.print("Button 2: ");
  //     Serial.print(button2.pressType);
  //     Serial.print(" ");
  //     Serial.println(currMillis - button2.onTime);
  //     button2.pressType = ' ';
  //   }

  //   button2.previous = button2.value;
  // }

  ///// both buttons held

  if (currMillis - button1.debounceStart > DEBOUNCE && currMillis - button2.debounceStart > DEBOUNCE) {
    if (button1.value == LOW && button2.value == LOW)  // Both buttons pressed
    {
      // {
      //   Serial.println("Both buttons held");   // Debug message for this point
      // }

      if (button1.previous == HIGH && button2.previous == HIGH)  // Both have just been pressed

        // {
        //   Serial.println("Both buttons held and just pressed");   // Debug message for this point
        // }

        button1.doubleHoldElapsed = currMillis;
      button1.debounceStart = currMillis;
      button2.doubleHoldElapsed = currMillis;
      button2.debounceStart = currMillis;
      {
        // Serial.println("Both buttons held");  // Debug message for this point
        // Serial.print("Button 1: ");
        // Serial.println(button1.doubleHoldElapsed);
        // Serial.print("Button 2: ");
        // Serial.println(button2.doubleHoldElapsed);
      }

      if (button1.doubleHoldElapsed >= doubleHoldTime && button2.doubleHoldElapsed >= doubleHoldTime) {
        button1.doublePress = 1;
        button2.doublePress = 1;
        Serial.println("Both buttons held for 3 seconds");
        digitalWrite(LED1, Is_On);
        delay(750);
        digitalWrite(LED1, Is_Off);

        if (button1.previous == HIGH && button2.previous == HIGH) {  // This doesn't reset the elapsed timer
          button1.doubleHoldElapsed = currMillis;
          button2.doubleHoldElapsed = currMillis;
        }
      }
    }
    //}
    //     if (button1.previous == HIGH && button2.previous == HIGH)  // both have just been released
    //       button1.pressType = ' ';
    //       button2.pressType = ' ';
    //       button1.doublePress = 0;
    //       button2.doublePress = 0;
    //   }
  }
}

Try this version. I've put the common code in a function (checkButton).

In the main loop I first check for the "double press"... if found I set the boolean... which is then checked when the button is released... if it is set then it just resets the boolean... but doesn't trigger one of the other button pressed states.

struct button
{
  int pin;
  int value;
  int previous;
  unsigned long onTime;
  unsigned long debounceStart;
  char pressType;
  boolean doublePress;
};

button button1 = {A1, HIGH, HIGH, 0, 0, ' ', false};           // Define a button.
button button2 = {A2, HIGH, HIGH, 0, 0, ' ', false};           // Define a button.

unsigned long currMillis;

const unsigned long DOUBLE_PRESS = 3000;               // Set as required.
const unsigned long LONG_PRESS   = 1000;               // Set as required.
const unsigned long DEBOUNCE     = 100;                // Set as required.

void setup()
{
  Serial.begin(115200);
  pinMode(button1.pin, INPUT_PULLUP);
  pinMode(button2.pin, INPUT_PULLUP);
  Serial.println("Started");
}

void loop()
{
  currMillis = millis();

  checkButton(button1);
  checkButton(button2);

  // Check for both buttons held down together.
  if (button1.value == LOW && currMillis - button1.onTime > DOUBLE_PRESS &&  // Button 1 held
      button2.value == LOW && currMillis - button2.onTime > DOUBLE_PRESS &&  // Button 2 held
      !button1.doublePress && !button2.doublePress)                          // To avoid triggering multiple times 
  {
    Serial.println("Both buttons held");
    
    button1.doublePress = true;
    button2.doublePress = true;
  }

  // Check button 1
  if (button1.pressType != ' ')
  {
    Serial.print("Button 1 ");
    Serial.print(button1.pressType);
    Serial.print(" ");
    Serial.println(currMillis - button1.onTime);
    button1.pressType = ' ';
  }

  // Check button 2
  if (button2.pressType != ' ')
  {
    Serial.print("Button 2 ");
    Serial.print(button2.pressType);
    Serial.print(" ");
    Serial.println(currMillis - button2.onTime);
    button2.pressType = ' ';
  }

  // Other code here...
}

void checkButton(button &passedButton)
{
  passedButton.value = digitalRead(passedButton.pin);

  if (currMillis - passedButton.debounceStart > DEBOUNCE)        // Don't do anything within the debounce time.
  {
    if (passedButton.value == LOW)                               // Is the button pressed?
    {

      if (passedButton.previous == HIGH)                         // Has it just been pressed?
      {
        passedButton.onTime        = currMillis;                 // Reset the time the button was first pressed.
        passedButton.debounceStart = currMillis;                 // Reset the debounce timer.
      }
    }
    else                                                         // Button must be released.
    {
      if (passedButton.previous == LOW)                          // Has it just been released?
      {
        if(passedButton.doublePress)                             // If it was double pressed then do nothing further.
          passedButton.doublePress = false;
        else if (currMillis - passedButton.onTime > LONG_PRESS)  // How long was it pressed?
          passedButton.pressType = 'L';
        else
          passedButton.pressType = 'S';

        passedButton.debounceStart = currMillis;                 // Reset the debounce timer.
      }
    }

    passedButton.previous = passedButton.value;                       // Keep track for next loop
  }

}

Thank you!

It works great.

Over the last couple of weeks I've been going over C tutorials, and the features in the new IDE are helpful too - especially hovering over a term and getting a quick explanation of what type it is and where it's referenced. So I'm able to follow this code much better than I would have been able to a few weeks ago.

I appreciate all the help you've given me.

1 Like

Thanks, this worked for me, too!

I was working with your code and trying to find a way to continually send a MIDI message. Using the println for testing I can't seem to find a way to print a line every delay(n) seconds without fouling up the double pressed feature. So far the only way I can get the code to print continually is by using if (button1.value == LOW && button1.doublePress != true). That leads to its own problems because it fires along with double press. Thoughts?

Thanks!

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.