Button Library for Multiple Button Pressed

I've been digging around and can't seem to find the right answer.

My goal: I have four momentary buttons that need to:

  1. Individually allow short press/release
  2. Individually allow long press/release
  3. Know when two buttons are short pressed together
  4. Know when two/three buttons are long pressed together

I'm having a heck of a time wrapping my head around this logically.

I currently have it working (using the Button.h library) with individual short and long presses, but I can't seem to figure out an easy way (not a HUGE nested if) to determine if more than one (and which ones) are currently pressed/released.

TIA,
Kyle

/*SwitchManager skeleton 
 
 
 This sketch is to introduce new people to the SwitchManager library written by Nick Gammon
 
 The library handles switch de-bouncing and provides timing and state change information in your sketch.
 The SwitchManager.h file should be placed in your libraries folder, i.e.
 C:\Users\YourName\Documents\Arduino\libraries\SwitchManager\SwitchManager.h
 You can download the library at:
 http://gammon.com.au/Arduino/SwitchManager.zip    Thank you Nick!
 
 In this example we have 2 normally open (N.O.) switches connected to the Arduino - increment and decrement.
 The increment switch will also be used as a "Reset" switch if pressed for more than two seconds.
 The two switches are connected between GND (0 volts) and an Arduino input pin.
 The library enables pull-up resistors for your switch inputs.
 Pushing a switch makes its pin LOW. Releasing a switch makes its pin HIGH.
 
 The SwitchManager library provides 10ms de-bounce for switches. 
 i.e. enum { debounceTime = 10, noSwitch = -1 };
 If you need more time, edit the SwitchManager.h file
 i.e. enum { debounceTime = 50, noSwitch = -1 }; //here it is changed to 50ms
 */

#include <SwitchManager.h>             
//object instantiations
SwitchManager myIncSwitch;
SwitchManager myDecSwitch;

unsigned long currentMillis;
unsigned long heartBeatMillis;
unsigned long heartFlashRate  = 500UL; // time the led will change state       
unsigned long incShortPress   = 500UL; // 1/2 second
unsigned long incLongPress    = 2000UL;// 2 seconds 
unsigned long decShortPress   = 500UL; // 1/2 second

const byte heartBeatLED       = 13;
const byte incSwitch          = 4; //increment switch is on Arduino pin 4
const byte decSwitch          = 5; //decrement switch is on Arduino pin 5

int myCounter;

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

void setup()
{
  Serial.begin(9600);

  //gives a visual indication if the sketch is blocking
  pinMode(heartBeatLED, OUTPUT);  

  myIncSwitch.begin (incSwitch, handleSwitchPresses); 
  myDecSwitch.begin (decSwitch, handleSwitchPresses);
  //the handleSwitchPresses() function is called when a switch changes state

} //                   E N D  O F  s e t u p ( )

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

void loop()
{
  //leave this line of code at the top of loop()
  currentMillis = millis();

  //***************************
  //some code to see if the sketch is blocking
  if (CheckTime(heartBeatMillis, heartFlashRate, true))
  {
    //toggle the heartBeatLED
    digitalWrite(heartBeatLED,!digitalRead(heartBeatLED));
  }

  //***************************
  //check to see what's happening with the switches
  //"Do not use delay()s" in your sketch as it will make switch changes unresponsive 
  //Use BlinkWithoutDelay (BWD) techniques instead.
  myIncSwitch.check ();  
  myDecSwitch.check (); 

  //***************************
  //put other non-blocking stuff here


} //                      E N D  O F  l o o p ( )


//======================================================================
//                          F U N C T I O N S
//======================================================================


//                        C h e c k T i m e ( ) 
//**********************************************************************
//Delay time expired function
//parameters:
//lastMillis = time we started
//wait = delay in ms
//restart = do we start again  

boolean CheckTime(unsigned long  & lastMillis, unsigned long wait, boolean restart) 
{
  //has time expired for this task?
  if (currentMillis - lastMillis >= wait) 
  {
    //should this start again? 
    if(restart)
    {
      //yes, get ready for the next iteration
      lastMillis = millis();  
    }
    return true;
  }
  return false;

} //                 E N D   o f   C h e c k T i m e ( )


//                h a n d l e S w i t c h P r e s s e s( )
//**********************************************************************

void handleSwitchPresses(const byte newState, const unsigned long interval, const byte whichPin)
{
  //  You get here "ONLY" if there has been a change in a switches state.

  //When a switch has changed state, SwitchManager passes this function 3 arguments:
  //"newState" this will be HIGH or LOW. This is the state the switch is in now.
  //"interval" the number of milliseconds the switch stayed in the previous state
  //"whichPin" is the switch pin that we are examining  

  switch (whichPin)
  {
    //***************************
    //are we dealing with this switch?
  case incSwitch: 

    //has this switch gone from LOW to HIGH (gone from pressed to not pressed)
    //this happens with normally open switches wired as mentioned at the top of this sketch
    if (newState == HIGH)
    {
      //The incSwitch was just released
      //was this a short press followed by a switch release
      if(interval <= incShortPress) 
      {
        Serial.print("My counter value is = ");
        myCounter++;
        if(myCounter > 1000)
        {
          //limit the counter to a maximum of 1000
          myCounter = 1000; 
        }
        Serial.println(myCounter);
      }

      //was this a long press followed by a switch release
      else if(interval >= incLongPress) 
        //we could also have an upper limit
        //if incLongMillis was 2000UL; we could then have a window between 2-3 seconds
        //else if(interval >= incLongMillis && interval <= incLongMillis + 1000UL) 
      {
        //this could be used to change states in a StateMachine
        //in this example however, we will just reset myCounter
        myCounter = 0;
        Serial.print("My counter value is = ");
        Serial.println(myCounter);
      }

    }

    //if the switch is a normally closed (N.C.) and opens on a press this section would be used
    //the switch must have gone from HIGH to LOW 
    else 
    {
      Serial.println("The incSwitch was just pushed");
    } 

    break; //End of case incSwitch

    //*************************** 
    //are we dealing with this switch?
  case decSwitch: 

    //has this switch gone from LOW to HIGH (gone from pressed to not pressed)
    //this happens with normally open switches wired as mentioned at the top of this sketch
    if (newState == HIGH)
    {
      //The decSwitch was just released
      //was this a short press followed by a switch release
      if(interval <= decShortPress) 
      {
        Serial.print("My counter value is = ");
        myCounter--;
        if(myCounter < 0) 
        {
          //don't go below zero
          myCounter = 0;
        }
        Serial.println(myCounter);
      }

    }

    //if the switch is a normally closed (N.C.) and opens on a press this section would be used
    //the switch must have gone from HIGH to LOW
    else 
    {
      Serial.println("The decSwitch switch was just pushed");
    } 

    break; //End of case decSwitch

    //*************************** 
    //Put default stuff here
    //default:
    //break; //END of default

  } //End switch (whichPin)

} //      E n d   o f   h a n d l e S w i t c h P r e s s e s ( )


//======================================================================
//                      E N D  O F  C O D E
//======================================================================

I guess that you should become familiar with automatons (state machines). Then every button change (idle-short-long) will change the state of the machine, and give an indication if some specific state is reached.

As another approach, encode the button states into one int value, with 2 bits for every button. Then a switch statement can be used, to detect specific states of that variable.

In either case you have to decide how to handle ambiguities and intermediate states. E.g. when 2 buttons are pressed long together, will this state override the actions for a long press of either button? Consider that before two buttons appear pressed long together, one button will appear long pressed before, while the other is not yet long pressed. Is it okay for your application if the action for a single button long press is executed first, later followed by the action for two buttons long pressed?

Larry, good to see you again. :slight_smile: While I didn't use that exact code, I have something similar and have been using it to handle exactly as it says: short presses on one or the other, and/or long presses. It didn't really help with simultaneous presses. I should probably include a sketch. Problem is, it's constantly been changing because I can't solidify how I want it to work yet.

DrD., I have been investigating and reading up on state machines (I will continue to do so). Stuff like this in C# seems easy, but in Arduino feels harder. Definitely learning.

I had already planned on using two bits per integer to save the state per button.

Your point about the ambiguities is what caused me to finally take to the forum. Your paragraph just confirmed that I have to determine how I want my device to work in such scenarios.

I'll continue thinking this through. The other thing I'm concerned about is SRAM usage. Between MIDI, wire, SDFat, and SSD1306Ascii, I'm running out of SRAM. :slight_smile:

So, here's what I've come up with. It works 98%.

ImprovedButton.h (based on GitHub - JChristensen/JC_Button: Arduino library to debounce button switches, detect presses, releases, and long presses)

#ifndef _IMPROVEDBUTTON_h
#define _IMPROVEDBUTTON_h

#if defined(ARDUINO) && ARDUINO >= 100
#include "arduino.h"
#else
#include "WProgram.h"
#endif
#include "delykLooperSettings.h"

class Button
{
private:
	uint8_t _multiBool;
	uint8_t _pin;           // Arduino pin number

	uint32_t _time;         // time of current state (all times are in ms)
	uint32_t _lastChange;   // time of last state change

public:
	Button();
	Button(uint8_t);

	uint8_t read(const uint32_t);
	uint8_t isPressed();
	uint8_t isReleased();
	uint8_t wasPressed();
	uint8_t wasReleased();
	uint8_t pressedFor(const uint32_t);
};
#endif

ImprovedButton.cpp

#include "ImprovedButton.h"
#define STATE_BIT 0
#define LAST_STATE_BIT 1
#define CHANGED_BIT 2

Button::Button() { }

Button::Button(uint8_t pin) {
	_multiBool = 0;
	_pin = pin;
	_time = _lastChange = millis();

	pinMode(pin, INPUT_PULLUP);

	uint8_t state = !digitalRead(pin);
	bitWrite(_multiBool, STATE_BIT, state);
	bitWrite(_multiBool, LAST_STATE_BIT, state);
	bitWrite(_multiBool, CHANGED_BIT, false);
}

uint8_t Button::read(const uint32_t ms = millis()) {
	_time = ms;

	if (ms - _lastChange < BUTTON_DEBOUNCE_TIME) {
		bitWrite(_multiBool, CHANGED_BIT, false);
	}
	else {
		bitWrite(_multiBool, LAST_STATE_BIT, bitRead(_multiBool, STATE_BIT));
		bitWrite(_multiBool, STATE_BIT, !digitalRead(_pin));

		if (bitRead(_multiBool, STATE_BIT) != bitRead(_multiBool, LAST_STATE_BIT)) {
			_lastChange = ms;
			bitWrite(_multiBool, CHANGED_BIT, true);
		}
		else {
			bitWrite(_multiBool, CHANGED_BIT, false);
		}
	}

	return bitRead(_multiBool, STATE_BIT);
}

uint8_t Button::isPressed() {
	return bitRead(_multiBool, STATE_BIT);
}

uint8_t Button::isReleased() {
	return !bitRead(_multiBool, STATE_BIT);
}

uint8_t Button::wasPressed() {
	return bitRead(_multiBool, STATE_BIT) && bitRead(_multiBool, CHANGED_BIT);
}

uint8_t Button::wasReleased() {
	return !bitRead(_multiBool, STATE_BIT) && bitRead(_multiBool, CHANGED_BIT);
}

uint8_t Button::pressedFor(const uint32_t ms) {
	return (isPressed() && _time - _lastChange >= ms);
}

delykStateMachine.h

#include "ImprovedButton.h"
#include "delykLooperSettings.h"

#ifndef _DELYKSTATEMACHINE_h
#define _DELYKSTATEMACHINE_h

#if defined(ARDUINO) && ARDUINO >= 100
#include "arduino.h"
#else
#include "WProgram.h"
#endif

class delykStateMachine {
private:
	Button * _buttons[7];

	// State Machine's State
	byte _currentState;
	
	// Button's States
	byte _longPressState;
	byte _wasReleasedState;
	byte _isReleasedState;
	byte _isPressedState;
	
	void _checkNeedRelease();
	void _checkLongPress();

	void(*_onShortPress)(uint8_t, uint8_t);
	void(*_onLongPress)(uint8_t, uint8_t);
	void(*_onMultiShortPress)(uint8_t, uint8_t);
	void(*_onEnterEditMode)();
	void(*_onCancelEditMode)();

public:
	enum {
		WAITING,
		IN_LONG_PRESS,
		ENTERING_EDIT_MODE,
		EDIT_MODE,
		EDIT_IN_LONG_PRESS,
		NEED_RELEASE,
		CANCELLING_EDIT_MODE
	};

	delykStateMachine();

	void loop(const uint32_t);

	void attachShortPressHandler(void(*)(uint8_t, uint8_t));
	void attachLongPressHandler(void(*)(uint8_t, uint8_t));
	void attachMultiShortPressHandler(void(*)(uint8_t, uint8_t));
	void attachEnterEditModeHandler(void(*)());
	void attachCancelEditModeHandler(void(*)());
};
#endif

delykStateMachine.cpp

#include "delykStateMachine.h"

Button * _buttons[7] = {
	new Button(SWITCH_A),
	new Button(SWITCH_B),
	new Button(SWITCH_C),
	new Button(SWITCH_D),
	new Button(SWITCH_BANK_DOWN),
	new Button(SWITCH_BANK_UP),
	new Button(SWITCH_TAP_TEMPO)
};

delykStateMachine::delykStateMachine() {
	_currentState = WAITING;
	_longPressState = 0;
	_wasReleasedState = 0;
	_isReleasedState = 0;
	_isPressedState = 0;

	this->_onCancelEditMode = NULL;
	this->_onEnterEditMode = NULL;
	this->_onLongPress = NULL;
	this->_onMultiShortPress = NULL;
	this->_onShortPress = NULL;
}

void delykStateMachine::_checkNeedRelease() {
	if (_longPressState > 0) {
		_currentState = IN_LONG_PRESS;
		_checkLongPress();
	}
	else if (_isReleasedState == 0b00001111) {
		_currentState = WAITING;
		//Serial.println(F("RELEASE --> WAITING"));
	}
}

void delykStateMachine::_checkLongPress() {
	if (_longPressState == 0b0001011) {
		_currentState = ENTERING_EDIT_MODE;
		_onEnterEditMode();
	}
	else if (_isReleasedState == 0b00001111) {
		_currentState = WAITING;
	}
}

void delykStateMachine::loop(const uint32_t ms) {
	byte idx;
	for (idx = 0; idx < 7; idx++) {
		_buttons[idx]->read(ms);
		bitWrite(_longPressState, idx, _buttons[idx]->pressedFor(1000));
		bitWrite(_wasReleasedState, idx, _buttons[idx]->wasReleased());
		bitWrite(_isReleasedState, idx, _buttons[idx]->isReleased());
		bitWrite(_isPressedState, idx, _buttons[idx]->isPressed());
	}

	switch (_currentState) {
	case WAITING:
		if (_longPressState > 0) {
			_currentState = IN_LONG_PRESS;
			_checkLongPress();
		}
		else if (_wasReleasedState > 0) {
			_onShortPress(_wasReleasedState, _currentState);
		}
		else if (__builtin_popcount(_isPressedState) > 1) {
			_currentState = NEED_RELEASE;
			_checkNeedRelease();
			_onMultiShortPress(_isPressedState, _currentState);
		}
		break;
	case NEED_RELEASE:
		_checkNeedRelease();
		break;
	case IN_LONG_PRESS:
		_checkLongPress();
		break;
	case ENTERING_EDIT_MODE:
		if (_isReleasedState == 0b00001111) {
			_currentState = EDIT_MODE;
		}
		break;
	case EDIT_MODE:
		if (_longPressState == 0b00001101) {
			_onCancelEditMode();
			_currentState = CANCELLING_EDIT_MODE;
		}
		else if (_longPressState > 0) {
			// TODO: Transitional state?
			_onLongPress(_wasReleasedState, _currentState);
		}
		else if (_wasReleasedState > 0) {
			_onShortPress(_wasReleasedState, _currentState);
		}
		// TODO: Commit changes?
		break;
	case CANCELLING_EDIT_MODE:
		if (_isReleasedState == 0b00001111) {
			_currentState = WAITING;
		}
		break;
	}
}

void delykStateMachine::attachShortPressHandler(void(*handler)(uint8_t, uint8_t)) {
	_onShortPress = handler;
}

void delykStateMachine::attachLongPressHandler(void(*handler)(uint8_t, uint8_t)) {
	_onLongPress = handler;
}

void delykStateMachine::attachMultiShortPressHandler(void(*handler)(uint8_t, uint8_t)) {
	_onMultiShortPress = handler;
}

void delykStateMachine::attachEnterEditModeHandler(void(*handler)()) {
	_onEnterEditMode = handler;
}

void delykStateMachine::attachCancelEditModeHandler(void(*handler)()) {
	_onCancelEditMode = handler;
}

main.ino

#include "delykLooperSettings.h"
#include "delykStateMachine.h"

unsigned long ms;
delykStateMachine dsm;

void setup() {
	dsm.attachCancelEditModeHandler(onCancelEditMode);
	dsm.attachEnterEditModeHandler(onEnterEditMode);
	dsm.attachLongPressHandler(onLongPress);
	dsm.attachMultiShortPressHandler(onMultiShortPress);
	dsm.attachShortPressHandler(onShortPress);
}

void loop() {
	ms = millis();
	dsm.loop(ms);
}

// State Machine/Button Events
void onShortPress(byte wasReleasedState, byte currentState) {
	if (currentState == delykStateMachine::EDIT_MODE) {
		//Serial.print(F("Short Enable/Disable Loop"));
		//Serial.println(blah21);
	}
	else {
		//Serial.print(F("Patch "));
		//Serial.println(blah21);
	}
}

void onLongPress(byte wasReleasedState, byte currentState) {
	if (currentState == delykStateMachine::EDIT_MODE) {
		//Serial.print(F("Long Enable/Disable Loop"));
		//Serial.println(blah21);
	}
}

void onEnterEditMode() {
	//Serial.println(F("EDIT MODE"));
}

void onCancelEditMode() {
	//Serial.println(F("CANCELLED"));
}

void onMultiShortPress(byte isPressedState, byte currentState) {
	if (isPressedState == 0b00001100) {
		//Serial.println(F("BANK UP"));
	}
	else if (isPressedState == 0b00000011) {
		//Serial.println(F("BANK DOWN"));
	}
}

It appears to compile with a RAM Usage of 66 bytes. Can I trim anymore fat? Please keep in mind I'm a C# dev, so some of this may not be proper C++.

The only scenario that doesn't work right is when I want to long push to get into EDIT MODE. If I don't push all three fast enough, it thinks I might be doing a BANK UP. I'm sure I can come up with some way to ignore the BANK UP, but I ran out of time last night.

Any tips or suggestions/code review is appreciated.