Tuesday, October 13, 2009

4-Way Button: Click, Double-Click, Hold, Long Hold


How many ways can a button be clicked? [Anonymous] commented asking about double-clicking after my "Click for A, Press and Hold for B" post, and I finally got around to it, adding a long hold function too. Last night I tidied up the code and debugged obscure outlier cases, and the result is a simple 4-way button function which can report click, double-click, hold, and long hold events.

When used with a rotary input (pot, rotary encoder, jog-shuttle, etc), complex branching menus are easy to implement. Note that the long hold event always triggers a "normal" hold event first, while single- and double-click are completely independent (as well as being independent from the hold events).

I've posted code in the first two comments-- the tabs of my test sketch, with the second comment containing the checkButton() function. This seems like a good candidate for my first proper library; I would appreciate any help in getting this example sketch converted into an easily instantiated object, like AlphaBeta's Button library.

34 comments:

  1. /* 4-Way Button: Click, Double-Click, Press+Hold, and Press+Long-Hold Test Sketch
    By Jeff Saltzman
    Oct. 13, 2009

    To keep a physical interface as simple as possible, this sketch demonstrates generating four output events from a single push-button.
    1) Click: rapid press and release
    2) Double-Click: two clicks in quick succession
    3) Press and Hold: holding the button down
    4) Long Press and Hold: holding the button down for a long time
    */

    #define buttonPin 19 // analog input pin to use as a digital input
    #define ledPin1 17 // digital output pin for LED 1
    #define ledPin2 16 // digital output pin for LED 2
    #define ledPin3 15 // digital output pin for LED 3
    #define ledPin4 14 // digital output pin for LED 4

    // LED variables
    boolean ledVal1 = false; // state of LED 1
    boolean ledVal2 = false; // state of LED 2
    boolean ledVal3 = false; // state of LED 3
    boolean ledVal4 = false; // state of LED 4

    //=================================================

    void setup()
    {
    // Set button input pin
    pinMode(buttonPin, INPUT);
    digitalWrite(buttonPin, HIGH );
    // Set LED output pins
    pinMode(ledPin1, OUTPUT);
    digitalWrite(ledPin1, ledVal1);
    pinMode(ledPin2, OUTPUT);
    digitalWrite(ledPin2, ledVal2);
    pinMode(ledPin3, OUTPUT);
    digitalWrite(ledPin3, ledVal3);
    pinMode(ledPin4, OUTPUT);
    digitalWrite(ledPin4, ledVal4);
    }

    void loop()
    {
    // Get button event and act accordingly
    int b = checkButton();
    if (b == 1) clickEvent();
    if (b == 2) doubleClickEvent();
    if (b == 3) holdEvent();
    if (b == 4) longHoldEvent();
    }

    //=================================================
    // Events to trigger by click and press+hold

    void clickEvent() {
    ledVal1 = !ledVal1;
    digitalWrite(ledPin1, ledVal1);
    }

    void doubleClickEvent() {
    ledVal2 = !ledVal2;
    digitalWrite(ledPin2, ledVal2);
    }

    void holdEvent() {
    ledVal3 = !ledVal3;
    digitalWrite(ledPin3, ledVal3);
    }

    void longHoldEvent() {
    ledVal4 = !ledVal4;
    digitalWrite(ledPin4, ledVal4);
    }

    ReplyDelete
  2. /*
    MULTI-CLICK: One Button, Multiple Events

    Oct 12, 2009
    Run checkButton() to retrieve a button event:
    Click
    Double-Click
    Hold
    Long Hold
    */

    // Button timing variables
    int debounce = 20; // ms debounce period to prevent flickering when pressing or releasing the button
    int DCgap = 250; // max ms between clicks for a double click event
    int holdTime = 2000; // ms hold period: how long to wait for press+hold event
    int longHoldTime = 5000; // ms long hold period: how long to wait for press+hold event

    // Other button variables
    boolean buttonVal = HIGH; // value read from button
    boolean buttonLast = HIGH; // buffered value of the button's previous state
    boolean DCwaiting = false; // whether we're waiting for a double click (down)
    boolean DConUp = false; // whether to register a double click on next release, or whether to wait and click
    boolean singleOK = true; // whether it's OK to do a single click
    long downTime = -1; // time the button was pressed down
    long upTime = -1; // time the button was released
    boolean ignoreUp = false; // whether to ignore the button release because the click+hold was triggered
    boolean waitForUp = false; // when held, whether to wait for the up event
    boolean holdEventPast = false; // whether or not the hold event happened already
    boolean longHoldEventPast = false;// whether or not the long hold event happened already

    int checkButton()
    {
    int event = 0;
    // Read the state of the button
    buttonVal = digitalRead(buttonPin);
    // Button pressed down
    if (buttonVal == LOW && buttonLast == HIGH && (millis() - upTime) > debounce) {
    downTime = millis();
    ignoreUp = false;
    waitForUp = false;
    singleOK = true;
    holdEventPast = false;
    longHoldEventPast = false;
    if ((millis()-upTime) < DCgap && DConUp == false && DCwaiting == true) DConUp = true;
    else DConUp = false;
    DCwaiting = false;
    }
    // Button released
    else if (buttonVal == HIGH && buttonLast == LOW && (millis() - downTime) > debounce) {
    if (not ignoreUp) {
    upTime = millis();
    if (DConUp == false) DCwaiting = true;
    else {
    event = 2;
    DConUp = false;
    DCwaiting = false;
    singleOK = false;
    }
    }
    }
    // Test for normal click event: DCgap expired
    if ( buttonVal == HIGH && (millis()-upTime) >= DCgap && DCwaiting == true && DConUp == false && singleOK == true) {
    event = 1;
    DCwaiting = false;
    }
    // Test for hold
    if (buttonVal == LOW && (millis() - downTime) >= holdTime) {
    // Trigger "normal" hold
    if (not holdEventPast) {
    event = 3;
    waitForUp = true;
    ignoreUp = true;
    DConUp = false;
    DCwaiting = false;
    //downTime = millis();
    holdEventPast = true;
    }
    // Trigger "long" hold
    if ((millis() - downTime) >= longHoldTime) {
    if (not longHoldEventPast) {
    event = 4;
    longHoldEventPast = true;
    }
    }
    }
    buttonLast = buttonVal;
    return event;
    }

    ReplyDelete
  3. Hi Jeff,
    Very clever button setup,
    I am right now re-writing a code to incorporate your Button code arrangement with and LCD.

    If you want I can give you a snippet of my code
    I'have been using..so far. Let me know....
    Thanks again.
    Frank..
    Australia
    framul@gmail.com

    ReplyDelete
  4. Hi Jeff,

    I applied the class to your code for my private usage. If you want to publish please contact me.

    Regards,
    Nadir
    catalkn@hotmail.com

    ReplyDelete
  5. I would love to have this as a class - yes please!

    ReplyDelete
  6. Hi James,

    I posted the class source to Jeff. I think that Jeff has right to publish.

    Regards,
    Nadir

    ReplyDelete
  7. Thanks, Nadir! James, I will try to publish it during the next week, but will add back the long hold since I need it for a project I'm working on, and it will give me some experience in editing the class structure. For now, using the multi-click code, and previously setting up buttonPin (set to HIGH for the internal pullup) should work fine.

    ReplyDelete
  8. Any news on this class? :)

    James

    ReplyDelete
  9. James,

    PM to me, I can sent it to you.

    Nadir

    catalkn@hotmail.com

    ReplyDelete
  10. Very cool code there!

    I get a feeling I'm missing something about how to use it (I am a newb). I pasted the top two comments into a sketch and it works, but your mention of "tabs" and "function" (IE: "I've posted code in the first two comments-- the tabs of my test sketch, with the second comment containing the checkButton() function.") make me think this is supposed to be in two parts, with the first part "calling" to the second part? Do I save the second comment (The "function") in a seperate text file or something?

    It works just pasted in, but would be a little easier that way.

    Thanks again, for both the code and letting me ask dumb questions, LOL. The overall goal is to make an RGB lightsaber LED controller, and this will let us do nested menus right on the device.. how cool is that?!

    TroyO
    troyollom@hotmail.com

    ReplyDelete
  11. I'd love to have a class version of this code.
    Is that possible? It looks above like it's been created, but I don't see where it may be available.

    Ron
    RonCraig007@yahoo.com

    ReplyDelete
  12. I've tried for two days to get a push and hold functionality in my latest project. I guess I have to accept that I am more mechanical than coding oriented. I wish I had found your code sooner. I want to know how easily I could get my hands on the nice clean arduino library if there is one?

    ReplyDelete
  13. Your code is very nice. I've used it in a Sous Vide project here to great success. Thanks a bunch for sharing!

    ReplyDelete
  14. hello,
    I translated your code into a library available at: http://pastebin.com/gQLTrHVF

    Thank you for your work

    Jeremy
    http://lightuino-stairlight.blogspot.com

    ReplyDelete
  15. sir jeff, do you have a separate snippet for the long press and hold? i'm really a newbie, i'm trying to extract but i'm confused with the mixed codes. thanks in advance. :)

    ReplyDelete
  16. I really hope you can help me, because I'm stumped here!

    I'm using your code above (thank you1), whittled down to detect just a single or double click, as that's all I need. It works perfectly.

    However, when I replace the button with a magnetic reed switch, it only *ever* detects a single click. And it's really frustrating!

    I've tried increasing the debounce time to 500ms and the DCgap to 2000ms, but to no avail. I can still do a (slow) double-click using a button, but when I use the reed switch, it's a single click every time.

    Any idea what might be causing the problem?

    ReplyDelete
    Replies
    1. What does a reed switch do really? If it's just a switch it should work fine, I've used this with different types of buttons. Debouncing it for very long like 500ms is too much though. I'd get a handle on what the reed switch is doing compared to a "regular" button. Good luck!

      Delete
    2. Some further info: according to http://reed-switch-info.com/ the bounce time for a typical reed switch is around 2ms, so it appears the bounce is irrelevant here.

      Which means... aargh, I have no idea what's going on!

      Using the reed switch to just turn an LED on and off works exactly like using a button.

      Using the reed switch with the above code triggers a single click as expected, exactly like using a button.

      But when I try a double click, it works perfectly with a button, and fails every time with a reed switch (triggering a single-click event instead).


      :-((

      Delete
  17. A reed switch is just a switch that's activated by a magnet. When a magnet comes near, it opens the switch; when the magnet goes away, it closes again (or vice versa, depending on the reed switch). They're useful for detecting doors closing, either to turn on a light, or to set off a burglar alarn.

    As far as I've ever known, it's just a switch exactly like any other — it can break or make a circuit — so I'm really confused about what's going on here.

    The reason I tried the outlandishly long debounce time is that literally the only thing I could think of was that just maybe the switch was prone to a lot of bounce, and this might be why the single/double click detection wasn't working, but I'm sure it can't be bouncing for 500ms or more, as you would actually see LEDs flickering at that point.

    I'm still bemused, so any other suggestions will be most welcome.

    ReplyDelete
  18. This is still driving me nuts, but in the interest of not clogging up Jeff's blog with long tangential comments, I've put a summary of the problem here:
    http://bit.ly/Lye6Aq

    If any readers have nothing better to do with half an hour and feel like diving into some (actually fairly simple) code to hunt down a bug, I'd be eternally grateful. I can trade you language services (put your app/site into French/Esperanto) or buy you dinner or something. :o) Please!

    ReplyDelete
    Replies
    1. Jeff ,
      This is very impressive and I am trying to incorporate it into a project I am working on with 4 button keypads . The embedded functions will help do the things that I need without adding more physical buttons. Thanks for sharing !

      Tim,
      Once a magnet reaches the threshold of attraction it will pull the reed making contact. To demo reed operation connect the switch to continuity function of your meter. Note how close a switch has to be to make contact , then start pulling the magnet away slowly and you will find the gap is typically larger for break. This is why a reed switch is great for security applications .

      Bob D

      Delete
  19. Hi, i'm new to arduino and programming.
    I'm trying to control only 2 leds with a button.
    control one of them with a single click and the other one with the double click.
    i tried modifying this code but didnt work for me.
    Will i only need to change the variables and the void setup, or are there changes that would have to be done to the void loop as well??

    ReplyDelete
    Replies
    1. Sorry without seeing your circuit it's hard to debug, but the sketch worked as shown so I'd try to get it working 1:1 before modifying anything!

      Delete
  20. Thanks Jeff.

    About the problems with the reed switch: Use a Hall effect switch. Not a linear Hall effect sensor but a switch.

    ReplyDelete
  21. OK, got it working. But how would I make ledPin3 only come on after the time alloted and as long as I am pushing the button. Does that make sense?

    My plan is to use this to start my truck with just one button. One click turns on my ACC, two clicks turns on the IGN, the long click will engage the starter, so I need that one to come on as usual but only stay on as long as I am pushing the button.

    Thanks for your time.
    Scott b.

    ReplyDelete
  22. hI,

    I just modified it for myself, but [newb] now i would like the same for a second button?
    could you help??

    // Read the state of the button
    buttonVal = digitalRead(buttonPin);
    // Button pressed down
    if (buttonVal == LOW && buttonLast == HIGH && (millis() - upTime) > debounce) {
    downTime = millis();
    ignoreUp = false;
    waitForUp = false;
    singleOK = true;
    holdEventPast = false;
    longHoldEventPast = false;
    if ((millis()-upTime) < DCgap && DConUp == false && DCwaiting == true) DConUp = true;
    else DConUp = false;
    DCwaiting = false;
    }
    // Button released
    else if (buttonVal == HIGH && buttonLast == LOW && (millis() - downTime) > debounce) {
    if (not ignoreUp) {
    upTime = millis();
    if (DConUp == false) DCwaiting = true;
    else {
    event = 2;
    DConUp = false;
    DCwaiting = false;
    singleOK = false;
    }
    }
    }
    // Test for normal click event: DCgap expired
    if ( buttonVal == HIGH && (millis()-upTime) >= DCgap && DCwaiting == true && DConUp == false && singleOK == true) {
    event = 1;
    DCwaiting = false;
    }
    // Test for hold
    if (buttonVal == LOW && (millis() - downTime) >= holdTime) {
    // Trigger "normal" hold
    if (not holdEventPast) {
    event = 3;
    waitForUp = true;
    ignoreUp = true;
    DConUp = false;
    DCwaiting = false;
    //downTime = millis();
    holdEventPast = true;
    }
    // Trigger "long" hold
    if ((millis() - downTime) >= longHoldTime) {
    if (not longHoldEventPast) {
    event = 4;
    longHoldEventPast = true;
    }
    }
    }
    buttonLast = buttonVal;
    return event;

    ReplyDelete
  23. Thank you very much for your code, this was exactly what I was looking for.
    Great Work!

    -X

    ReplyDelete
  24. Hi Jeff,
    been wanting to minimize my projects by removing my 4 microswitches to now just 1. Looking forward to implementing your code. Thanks!

    ReplyDelete
  25. Hi. Great Work.
    I do have a question.
    I can see how your code works for a single button or even several buttons, but each connected to it's Arduino pin.

    Is there a way to make this work with 8 buttons connected to a Multiplexer (so all being read from one single AnalogRead)? That would be so great!!!

    Thanks!
    =)

    ReplyDelete
    Replies
    1. Of course! It all depends on how you read the button. You could wrap it up a bunch of ways, but this is where it would help to make a library for your specific application... which is up to you ;)

      Delete
  26. Some of my comments:
    1, it looks like the variable: waitForUp is an unused variable
    2, DCwaiting is actually a variable waiting for the second press down edge, right? (In double click mode)
    3, DConUp is actually a variable waiting for the second press release edge, right? (In double click mode)

    Both of the two variable was used in checking double click mode, so I suggest change their names:
    DCwaiting -> waitForNextDown
    DConUp -> waitForNextUp

    Thanks.

    ReplyDelete
  27. BTW: I think you can use a enum instead of numbers, like:
    enum KeyEvent
    {
    Undefined = 0;
    SingleClick = 1;
    DoubleClick = 2;
    PressHold = 3;
    LongPressHold = 4;
    };

    ReplyDelete
  28. So, lets step away from multiple outputs and look at the following scenario.
    One button, single click blinks led 3 times
    1000 millis hold blinks led until stopped by a second switch? Can you help? Cause I'm about to snap trying to figure it out.

    ReplyDelete
    Replies
    1. Hi Adam, interesting problem, sounds very doable! Seems like you have a clear spec, so what's the problem exactly? I wonder if you're using the dreaded delay() to make the LED blink: your sketch can't detect a button state change if you're in the delay(). Keeping track of timing and looping your sketch in the KHz range makes simple work of catching any change in UI element states.

      Delete