An adjustable, compact, event-driven button library for Arduino that debounces and dispatches events to a user-defined event handler.
An adjustable, compact, event-driven button library for Arduino platforms.
This library provides classes which accept inputs from a mechanical button connected to a digital input pin on the Arduino. The library should be able to handle momentary buttons, maintained buttons, and switches, but it was designed primarily for momentary buttons.
The library is named "AceButton" because:
EventHandlercallback function
Most of the features of the library can be accessed through 2 classes, 1 callback function, and 1 interface:
AceButton(class)
ButtonConfig(class)
EventHandler(typedef)
IEventHandler(interface)
The
AceButtonclass contains the logic for debouncing and determining if a particular event has occurred.
The
ButtonConfigclass holds various timing parameters, the event handler, code for reading the button, and code for getting the internal clock.
The
EventHandleris a user-defined callback function with a specific signature which is registered with the
ButtonConfigobject. When the library detects interesting events, the callback function is called by the library, allowing the client code to handle the event.
The
IEventHandleris an interface (pure abstract class) that provides an alternative to the
EventHandler. Instead of using a callback function, an object of type
IEventHandlercan be used to handle the button events.
The supported events are:
AceButton::kEventPressed
AceButton::kEventReleased
AceButton::kEventClicked
AceButton::kEventDoubleClicked
AceButton::kEventLongPressed
AceButton::kEventRepeatPressed
AceButton::kEventLongReleased(v1.8)
The basic
ButtonConfigclass assumes that each button is connected to a single digital input pin. In some situations, the number of buttons that we want is greater than the number of input pins available. This library provides 2 subclasses of
ButtonConfigwhich may be useful:
EncodedButtonConfig
2^N - 1buttons using
Npins (e.g. 7 buttons using 3 digital pins).
LadderButtonConfig
analogRead()method is used to read the different voltage levels corresponding to each button.
Both
EncodedButtonConfigand
LadderButtonConfigsupport all 7 events listed above (e.g.
kEventClickedand
kEventDoubleClicked).
Version: 1.8.2 (2021-01-22)
Changelog: CHANGELOG.md
Here are the high-level features of the AceButton library:
EventHandlercallback function
IEventHandler(>= v1.6)
kEventPressed
kEventReleased
kEventClicked
kEventDoubleClicked
kEventLongPressed
kEventRepeatPressed
kEventLongReleased
digitalRead()button read function can be overridden
millis()clock function can be overridden
AceButtonconsumes 14 bytes (8-bit) or 16 bytes (32-bit)
ButtonConfigconsumes 18 bytes (8-bit) or 24 bytes (32-bit)
ButtonConfiginstance created automatically by the library
AceButton::check()
Compared to other Arduino button libraries, I think the unique or exceptional features of the AceButton library are:
Here is a simple program (see examples/HelloButton) which controls the builtin LED on the Arduino board using a momentary button connected to PIN 2.
#include using namespace ace_button;const int BUTTON_PIN = 2; const int LED_ON = HIGH; const int LED_OFF = LOW;
AceButton button(BUTTON_PIN);
void handleEvent(AceButton*, uint8_t, uint8_t);
void setup() { pinMode(LED_BUILTIN, OUTPUT); pinMode(BUTTON_PIN, INPUT_PULLUP); button.setEventHandler(handleEvent); }
void loop() { button.check(); }
void handleEvent(AceButton* /button/, uint8_t eventType, uint8_t /buttonState/) { switch (eventType) { case AceButton::kEventPressed: digitalWrite(LED_BUILTIN, LED_ON); break; case AceButton::kEventReleased: digitalWrite(LED_BUILTIN, LED_OFF); break; } } </acebutton.h>
(The
buttonand
buttonStateparameters are commented out to avoid an
unused parameterwarning from the compiler. We can't remove the parameters completely because the method signature is defined by the
EventHandlertypedef.)
The latest stable release is available in the Arduino IDE Library Manager. Search for "AceButton". Click install.
The development version can be installed by cloning the GitHub repository (https://github.com/bxparks/AceButton), checking out the
developbranch, then manually copying over the contents to the
./librariesdirectory used by the Arduino IDE. (The result is a directory named
./libraries/AceButton.)
The
masterbranch contains the tagged stable releases.
The core of the library is self-contained and has no external dependencies.
The some programs in
examples/may depend on: * AceCommon (https://github.com/bxparks/AceCommon)
The unit tests under
testsdepend on: * AUnit (https://github.com/bxparks/AUnit)
The source files are organized as follows: *
src/AceButton.h- main header file *
src/ace_button/- all implementation files *
src/ace_button/testing/- internal testing files *
tests/- unit tests which require AUnit *
examples/- example sketches
The following example sketches are provided:
IEventHandler
AceButton:check()with a start/stop/reset button
kFeatureLongPress
ButtonConfigand
EventHandlerinstances
getId()
kFeatureLongPress,
kFeatureRepeatPress,
kFeatureSuppressAfterLongPress, and
kFeatureSuppressAfterRepeatPress
AceButtonand initialize them using the
init()method in a loop
kEventClickedfrom a
kEventDoubleClickedusing a
kEventReleasedinstead
kEventClickedfrom a
kEventDoubleClickedusing the
kFeatureSuppressClickBeforeDoubleClickflag at the cost of increasing the response time of the
kEventClickedevent
Encoded4To2ButtonConfigand
Encoded8To3Buttonconfigclasses to decode
M=3buttons with
N=2pins, or
M=7buttons with
N=3pins
EncodedButtonConfigclass to handle
M=15buttons with
N=4pins
analogRead()
AceButton::check()method for various types of events (idle, press/release, click, double-click, and long-press)
There are 2 classes and one typedef that a user will normally interact with:
AceButton(class)
ButtonConfig(class)
EventHandler(typedef)
Advanced usage is supported by:
EncodedButtonConfig- binary encoded buttons supporting
2^N-1buttons on
Ndigital pins
LadderButtonConfig- resistor ladder buttons using analog pins
We explain how to use these below.
Only a single header file
AceButton.his required to use this library. To prevent name clashes with other libraries that the calling code may use, all classes are defined in the
ace_buttonnamespace. To use the code without prepending the
ace_button::prefix, use the
usingdirective:
#include using namespace ace_button;
If you are dependent on just
AceButton, the following might be sufficient:
#include using ace_button::AceButton;
The
ButtonConfigclass supports the simplest wiring. Each button is connected to a single digital input pin, as shown below. In the example below, 3 buttons labeled
S0,
S1and
S2are connected to digital input pins
D2,
D3, and
D4:
An Arduino microcontroller pin can be in an
OUTPUTmode, an
INPUTmode, or an
INPUT_PULLUPmode. This mode is controlled by the
pinMode()method.
By default upon boot, the pin is set to the
INPUTmode. However, this
INPUTmode puts the pin into a high impedance state, which means that if there is no wire connected to the pin, the voltage on the pin is indeterminant. When the input pin is read (using
digitalRead()), the boolean value will be a random value. If you are using the pin in
INPUTmode, you must connect an external pull-up resistor (connected to Vcc) or pull-down resistor (connected to ground) so that the voltage level of the pin is defined when there is nothing connected to the pin (i.e. when the button is not pressed).
The
INPUT_PULLUPmode is a special
INPUTmode which tells the microcontroller to connect an internal pull-up resistor to the pin. It is activated by calling
pinMode(pin, INPUT_PULLUP)on the given
pin. This mode is very convenient because it eliminates the external resistor, making the wiring simpler.
The 3 resistors
Rc1,
Rc2andc
Rc3are optional current limiting resistors. They help protect the microcontroller in the case of misconfiguration. If the pins are accidentally set to
OUTPUTmode, then pressing one of the buttons would connect the output pin directly to ground, causing a large amount of current to flow that could permenantly damage the microcontroller. The resistance value of 220 ohms (or maybe 330 ohms) is high enough to keep the current within safety limits, but low enough compared to the internal pullup resistor that it is able to pull the digital pin to a logical 0 level. These current limiting resistors are good safety measures, but I admit that I often get lazy and don't use them when doing quick experiments.
The AceButton library itself does not call the
pinMode()function. The calling application is responsible for calling
pinMode(). Normally, this happens in the global
setup()method but the call can happen somewhere else if the application requires it. The reason for decoupling the hardware configuration from the AceButton library is mostly because the library does not actually care about the specific hardware wiring of the button. It does not care whether an external resistor is used, or the internal resistor is used. It only cares about whether the resistor is a pull-up or a pull-down.
See https://www.arduino.cc/en/Tutorial/DigitalPins for additional information about the I/O pins on an Arduino.
The
AceButtonclass looks like this (not all public methods are shown): ```C++ namespace ace_button {
class AceButton { public: static const uint8t kEventPressed = 0; static const uint8t kEventReleased = 1; static const uint8t kEventClicked = 2; static const uint8t kEventDoubleClicked = 3; static const uint8t kEventLongPressed = 4; static const uint8t kEventRepeatPressed = 5; static const uint8t kEventLongReleased = 6; static const uint8t kButtonStateUnknown = 127;
explicit AceButton(uint8_t pin = 0, uint8_t defaultReleasedState = HIGH, uint8_t id = 0); explicit AceButton(ButtonConfig* buttonConfig, uint8_t pin = 0, uint8_t defaultReleasedState = HIGH, uint8_t id = 0); void init(uint8_t pin = 0, uint8_t defaultReleasedState = HIGH, uint8_t id = 0); void init(ButtonConfig* buttonConfig, uint8_t pin = 0, uint8_t defaultReleasedState = HIGH, uint8_t id = 0);ButtonConfig* getButtonConfig(); void setButtonConfig(ButtonConfig* buttonConfig); void setEventHandler(ButtonConfig::EventHandler eventHandler);
uint8_t getPin(); uint8_t getDefaultReleasedState(); uint8_t getId();
void check();
};
} ```
Each physical button will be handled by an instance of
AceButton. At a minimum, the instance needs to be told the pin number of the button. This can be done through the constructor:
const uint8_t BUTTON_PIN = 2;AceButton button(BUTTON_PIN);
void setup() { pinMode(BUTTON_PIN, INPUT_PULLUP); ... }
Or we can use the
init()method in the
setup():
AceButton button;void setup() { pinMode(BUTTON_PIN, INPUT_PULLUP); button.init(BUTTON_PIN); ... }
Both the constructor and the
init()function take 3 optional parameters as shown above:
pin: the I/O pin number assigned to the button
defaultReleasedState: the logical value of the button when it is in its default "released" state (
HIGHusing a pull-up resistor,
LOWfor a pull-down resistor)
id: an optional, user-defined identifier for the the button, for example, an index into an array with additional information
The
pinmust be defined either through the constructor or the
init()method. But the other two parameters may be optional in many cases.
Finally, the
AceButton::check()method should be called from the
loop()method periodically. Roughly speaking, this should be about 4 times faster than the value of
getDebounceDelay()so that the various event detection logic can work properly. (If the debounce delay is 20 ms,
AceButton::check()should be called every 5 ms or faster.)
void loop() { ... button.check(); ... }
Warning:
If you attempt to use Pin 0 in the
AceButton()constructor:
C++ AceButton button(0);you may encounter a compile-time error such as this:
error: call of overloaded 'AceButton(int)' is ambiguous
The solution is to explicitly cast the
0to a
uint8_ttype, or to assign it explicitly to a
uint8_tconst, like this: ```C++ // Explicit cast AceButton button((uint8_t) 0);
// Or assign to a const first. static const uint8_t PIN = 0; AceButton button(PIN);
See [Issue #40](https://github.com/bxparks/AceButton/issues/40) for details.ButtonConfig Class
The core concept of the AceButton library is the separation of the button (
AceButton
) from its configuration (ButtonConfig
).
AceButton
class has the logic for debouncing and detecting the various
events (Pressed, Released, etc), and the various bookkeeping variables
needed to implement the logic. These variables are associated with the
specific instance of that AceButton
.ButtonConfig
class has the various timing parameters which control
how much time is needed to detect certain events. This class also has the
ability to override the default methods for reading the pin (readButton()
)
and the clock (getClock()
). This ability allows unit tests to be written.The class looks like this (not all public methods are shown):
```C++ namespace ace_button {
class ButtonConfig { public: static const uint16_t kDebounceDelay = 20; static const uint16_t kClickDelay = 200; static const uint16_t kDoubleClickDelay = 400; static const uint16_t kLongPressDelay = 1000; static const uint16_t kRepeatPressDelay = 1000; static const uint16_t kRepeatPressInterval = 200;
typedef uint16_t FeatureFlagType;
static const FeatureFlagType kFeatureClick = 0x01;
static const FeatureFlagType kFeatureDoubleClick = 0x02;
static const FeatureFlagType kFeatureLongPress = 0x04;
static const FeatureFlagType kFeatureRepeatPress = 0x08;
static const FeatureFlagType kFeatureSuppressAfterClick = 0x10;
static const FeatureFlagType kFeatureSuppressAfterDoubleClick = 0x20;
static const FeatureFlagType kFeatureSuppressAfterLongPress = 0x40;
static const FeatureFlagType kFeatureSuppressAfterRepeatPress = 0x80;
static const FeatureFlagType kFeatureSuppressClickBeforeDoubleClick = 0x100;
static const FeatureFlagType kFeatureSuppressAll =
(kFeatureSuppressAfterClick |
kFeatureSuppressAfterDoubleClick |
kFeatureSuppressAfterLongPress |
kFeatureSuppressAfterRepeatPress |
kFeatureSuppressClickBeforeDoubleClick);
typedef void (*EventHandler)(AceButton* button, uint8_t eventType,
uint8_t buttonState);
ButtonConfig() = default;
uint16_t getDebounceDelay();
uint16_t getClickDelay();
uint16_t getDoubleClickDelay();
uint16_t getLongPressDelay();
uint16_t getRepeatPressDelay();
uint16_t getRepeatPressInterval();
void setDebounceDelay(uint16_t debounceDelay);
void setClickDelay(uint16_t clickDelay) {
void setDoubleClickDelay(uint16_t doubleClickDelay);
void setLongPressDelay(uint16_t longPressDelay);
void setRepeatPressDelay(uint16_t repeatPressDelay);
void setRepeatPressInterval(uint16_t repeatPressInterval);
virtual unsigned long getClock();
virtual int readButton(uint8_t pin);
bool isFeature(FeatureFlagType features);
void setFeature(FeatureFlagType features);
void clearFeature(FeatureFlagType features);
void resetFeatures();
void setEventHandler(EventHandler eventHandler);
static ButtonConfig* getSystemButtonConfig();
};
}
The
ButtonConfig(or a customized subclass) can be created and assigned to one or more
AceButtoninstances using dependency injection through the
AceButton(ButtonConfig*)constructor. This constructor also accepts the same
(pin, defaultReleasedState, id)parameters as
init(pin, defaultReleasedState, id)method. Sometimes it's easier to set all the parameters in one place using the constructor. Other times, the parameters are not known until the
AceButton::init()method can be called from the global
setup()method.
const uint8_t PIN1 = 2; const uint8_t PIN2 = 4;ButtonConfig buttonConfig; AceButton button1(&buttonConfig, PIN1); AceButton button2(&buttonConfig, PIN2);
void setup() { pinMode(PIN1, INPUT_PULLUP); pinMode(PIN2, INPUT_PULLUP); ... }
Another way to inject the
ButtonConfigdependency is to use the
AceButton::setButtonConfig()method but it is recommended that you use the constructor instead because the dependency is easier to follow.
A single instance of
ButtonConfigcalled the "System ButtonConfig" is automatically created by the library at startup. By default, all instances of
AceButtonare automatically assigned to this singleton instance. We explain in the Single Button Simplifications section below how this simplifies the code needed to handle a single button.
The
ButtonConfigclass provides a number of methods which are mostly used internally by the
AceButtonclass. The one method which is expected to be used by the calling client code is
setEventHandler()which assigns the user-defined
EventHandlercallback function to the
ButtonConfiginstance. This is explained in more detail below in the EventHandler section below.
Here are the methods to retrieve the timing parameters:
uint16_t getDebounceDelay();(default: 20 ms)
uint16_t getClickDelay();(default: 200 ms)
uint16_t getDoubleClickDelay();(default: 400 ms)
uint16_t getLongPressDelay();(default: 1000 ms)
uint16_t getRepeatPressDelay();(default: 1000 ms)
uint16_t getRepeatPressInterval();(default: 200 ms)
The default values of each timing parameter can be changed at run-time using the following methods:
void setDebounceDelay(uint16_t debounceDelay);
void setClickDelay(uint16_t clickDelay);
void setDoubleClickDelay(uint16_t doubleClickDelay);
void setLongPressDelay(uint16_t longPressDelay);
void setRepeatPressDelay(uint16_t repeatPressDelay);
void setRepeatPressInterval(uint16_t repeatPressInterval);
The
ButtonConfigclass has 2 methods which provide hooks to its external hardware dependencies:
virtual unsigned long getClock();
virtual int readButton(uint8_t pin);
By default these are mapped to the underlying Arduino system functions respectively:
millis()
digitalRead()
Unit tests are possible because these methods are
virtualand the hardware dependencies can be swapped out with fake ones.
We have assumed that there is a 1-to-many relationship between a
ButtonConfigand the
AceButton. In other words, multiple buttons will normally be associated with a single configuration. Each
AceButtonhas a pointer to an instance of
ButtonConfig. So the cost of separating the
ButtonConfigfrom
AceButtonis 2 bytes in each instance of
AceButton. Note that this is equivalent to adding virtual methods to
AceButton(which would add 2 bytes), so in terms of static RAM size, this is a wash.
The library is designed to handle multiple buttons, and it assumes that the buttons are normally grouped together into a handful of types. For example, consider the buttons of a car radio. It has several types of buttons:
In this example, there are 9 buttons, but only 3 instances of
ButtonConfigwould be needed.
The event handler is a callback function that gets called when the
AceButtonclass determines that an interesting event happened on the button. The advantage of this mechanism is that all the complicated logic of determining the various events happens inside the
AceButtonclass, and the user will normally not need to worry about the details.
The event handler is defined in the
ButtonConfigclass and has the following signature:
class ButtonConfig { public: typedef void (*EventHandler)(AceButton* button, uint8_t eventType, uint8_t buttonState); ... };
The event handler is registered with the
ButtonConfigobject, not with the
AceButtonobject, although the convenience method
AceButton::setEventHandler()is provided as a pass-through to the underlying
ButtonConfig(see the Single Button Simplifications section below):
ButtonConfig buttonConfig;void handleEvent(AceButton* button, uint8_t eventType, uint8_t buttonState) { ... }
void setup() { ... buttonConfig.setEventHandler(handleEvent); ... }
The motivation for this design is to save static memory. If multiple buttons are associated with a single
ButtonConfig, then it is not necessary for every button of that type to hold the same pointer to the
EventHandlerfunction. It is only necessary to save that information once, in the
ButtonConfigobject.
Pro Tip 1: Comment out the unused parameter(s) in the
handleEvent()method to avoid the
unused parametercompiler warning:
C++ void handleEvent(AceButton* /*button*/, uint8_t eventType, uint8_t /*buttonState*/) { ... }The Arduino sketch compiler can get confused with the parameters commented out, so you may need to add a forward declaration for the
handleEvent()method before the
setup()method:
C++ void handleEvent(AceButton*, uint8_t, uint8_t);
Pro Tips 2: The event handler can be an object instead of just a function pointer. An object-based event handler can be useful in more complex applications with numerous buttons. See the section on Object-based Event Handler in the Advanced Topics below.
The
EventHandlerfunction receives 3 parameters from the
AceButton:
button
AceButtoninstance that generated this event
getPin()or the
getId()
eventType
AceButton::kEventXxxconstants
buttonState
HIGHor
LOWbutton state that generated this event
The
buttonpointer should be used only to extract information about the button that triggered the event. It should not be used to modify the button's internal variables in any way within the eventHandler. The logic in
AceButton::check()assumes that those internal variable are held constant, and if they are changed by the eventHandler, unpredictable results may occur. (I should have made the
buttonbe a
const AceButton*but by the time I realized this, there were too many users of the library already, and I did not want to make a breaking change to the API.)
If you are using only a single button, then you should need to check only the
eventType.
It is not expected that
buttonStatewill be needed very often. It should be sufficient to examine just the
eventTypeto determine the action that needs to be performed. Part of the difficulty with this parameter is that it has the value of
LOWor
HIGH, but the physical interpretation of those values depends on whether the button was wired with a pull-up or pull-down resistor. Use the helper function
button->isReleased(buttonState)to translate the raw
buttonStateinto a more meaningful determination if you need it.
Only a single
EventHandlerper
ButtonConfigis supported. An alternative would have been to register a separate event handler for each of the 6
kEventXxxevents. But each callback function requires 2 bytes of memory, and it was assumed that in most cases, the calling client code would be interested in only a few of these event types, so it seemed wasteful to allocate 12 bytes when most of these would be unused. If the client code really wanted separate event handlers, it can be easily emulated by invoking them through the main event handler:
void handleEvent(AceButton* button, uint8_t eventType, uint8_t buttonState) { switch (eventType) { case AceButton::kEventPressed: handleEventPressed(button, eventType, buttonState); break; case AceButton::kEventReleased: handleEventReleased(button, eventType, buttonState); break; ... } }
The Arduino runtime environment is single-threaded, so the
EventHandleris called in the middle of the
AceButton::check()method, in the same thread as the
check()method. It is therefore important to write the
EventHandlercode to run somewhat quickly, so that the delay doesn't negatively impact the logic of the
AceButton::check()algorithm. Since
AceButton::check()should run approximately every 5 ms, the user-provided
EventHandlershould run somewhat faster than 5 ms. Given a choice, it is probably better to use the
EventHandlerto set some flags or variables and return quickly, then do additional processing from the
loop()method.
Sometimes it is too convenient or unavoidable to perform a long-running operation inside the event handler (e.g. making an HTTP). This is fine, I have done this occasionally. Just be aware that the button scanning operation will not work during that long-running operation.
Speaking of threads, the API of the AceButton Library was designed to work in a multi-threaded environment, if that situation were to occur in the Arduino world.
The supported events are defined by a list of constants in
AceButton.h:
AceButton::kEventPressed(always enabled, cannot be suppressed)
AceButton::kEventReleased(default: enabled)
AceButton::kEventClicked(default: disabled)
AceButton::kEventDoubleClicked(default: disabled)
AceButton::kEventLongPressed(default: disabled)
AceButton::kEventRepeatPressed(default: disabled)
AceButton::kEventLongReleased(default: disabled, autoenabled by
kFeatureSuppressAfterLongPress, new for v1.8)
These values are sent to the
EventHandlerin the
eventTypeparameter.
Two of the events are enabled by default, four are disabled by default but can be enabled by using a Feature flag described below.
There are 9 flags defined in
ButtonConfigwhich can control the behavior of
AceButtonevent handling:
ButtonConfig::kFeatureClick
ButtonConfig::kFeatureDoubleClick
ButtonConfig::kFeatureLongPress
ButtonConfig::kFeatureRepeatPress
ButtonConfig::kFeatureSuppressAfterClick
ButtonConfig::kFeatureSuppressAfterDoubleClick
ButtonConfig::kFeatureSuppressAfterLongPress
ButtonConfig::kFeatureSuppressAfterRepeatPress
ButtonConfig::kFeatureSuppressClickBeforeDoubleClick
ButtonConfig::kFeatureSuppressAll
These constants are used to set or clear the given flag:
// Get the current config. ButtonConfig* config = button.getButtonConfig();// Set a specific feature config->setFeature(ButtonConfig::kFeatureLongPress);
// Clear a specific feature config->clearFeature(ButtonConfig::kFeatureLongPress);
// Test for a specific feature if (config->isFeature(ButtonConfig::kFeatureLongPress)) { ... }
// Clear all features config->resetFeatures()
The meaning of these flags are described below.
Of the 7 event types, 5 are disabled by default:
AceButton::kEventClicked
AceButton::kEventDoubleClicked
AceButton::kEventLongPressed
AceButton::kEventRepeatPressed
AceButton::kEventLongReleased
To receive these events, call
ButtonConfig::setFeature()with the following corresponding flags:
ButtonConfig::kFeatureClick
ButtonConfig::kFeatureDoubleClick
ButtonConfig::kFeatureLongPress
ButtonConfig::kFeatureRepeatPress
ButtonConfig::kFeatureSuppressAfterLongPress(suppresses
kEventReleasedafter a LongPress, but turns on
kEventLongReleasedas a side effect)
To disable these events, call
ButtonConfig::clearFeature()with one of these flags.
Enabling
kFeatureDoubleClickautomatically enables
kFeatureClick, because we need to have a Clicked event before a DoubleClicked event can be detected.
It seems unlikely that both
LongPressand
RepeatPressevents would be useful at the same time, but both event types can be activated if you need it.
Event types can be considered to be built up in layers, starting with the lowest level primitive events: Pressed and Released. Higher level events are built on top of the lower level events through various timing delays. When a higher level event is detected, it is sometimes useful to suppress the lower level event that was used to detect the higher level event.
For example, a Clicked event requires a Pressed event followed by a Released event within a
ButtonConfig::getClickDelay()milliseconds (200 ms by default). The Pressed event is always generated. If a Clicked event is detected, we could choose to generate both a Released event and a Clicked event, and this is the default behavior.
However, many times, it is useful to suppress the Released event if the Clicked event is detected. The
ButtonConfigcan be configured to suppress these lower level events. Call the
setFeature(feature)method passing the various
kFeatureSuppressXxxconstants:
ButtonConfig::kFeatureSuppressAfterClick
kEventReleasedevent after a Clicked event is detected
kFeatureDoubleClickautomatically enables
kFeatureClick
ButtonConfig::kFeatureSuppressAfterDoubleClick
kEventReleasedevent and the second Clicked event if a DoubleClicked event is detected
ButtonConfig::kFeatureSuppressAfterLongPress
kEventReleasedevent if a LongPressed event is detected
kEventLongReleasedevent as a substitute for the suppressed
kEventReleased, see Distinguishing Pressed and Long Pressed subsection below for more details.
ButtonConfig::kFeatureSuppressAfterRepeatPress
kEventReleasedevent after the last RepeatPressed event
ButtonConfig::kFeatureSuppressClickBeforeDoubleClick
kEventClickedevent is postponed by
getDoubleClickDelay()millis until the code can determine if a DoubleClick has occurred. If so, then the postponed
kEventClickedmessage to the
EventHandleris suppressed.
ButtonConfig::kFeatureSuppressAll
By default, no suppression is performed.
As an example, to suppress the
kEventReleasedafter a
kEventLongPressed(this is actually often the case), you would do this:
ButtonConfig* config = button.getButtonConfig(); config->setFeature(ButtonConfig::kFeatureSuppressAfterLongPress);
The special convenient constant
kFeatureSuppressAllis equivalent of using all suppression constants:
ButtonConfig* config = button.getButtonConfig(); config->setFeature(ButtonConfig::kFeatureSuppressAll);
All suppressions can be cleared by using:
C++ ButtonConfig* config = button.getButtonConfig(); config->clearFeature(ButtonConfig::kFeatureSuppressAll);
Note, however, that the
isFeature(ButtonConfig::kFeatureSuppressAll)currently means "isAnyFeature() implemented?" not "areAllFeatures() implemented?" We don't expect
isFeature()to be used often (or at all) for
kFeatureSuppressAll.
You can clear all feature at once using:
C++ ButtonConfig* config = button.getButtonConfig(); config->resetFeatures();This is useful if you want to reuse a
ButtonConfiginstance and you want to reset its feature flags to its initial state.
Although the AceButton library is designed to shine for multiple buttons, you may want to use it to handle just one button. The library provides some features to make this simple case easy.
ButtonConfigcalled a "System ButtonConfig". This System ButtonConfig can be retrieved using the class static method
ButtonConfig::getSystemButtonConfig().
AceButtonis assigned an instance of the System ButtonConfig by default (which can be overridden manually).
EventHandlerfor the System ButtonConfig to be set easily through
AceButtonitself, instead of having to get the System ButtonConfig first, then set the event handler. In other words,
button.setEventHandler(handleEvent)is a synonym for
button.getButtonConfig()->setEventHandler(handleEvent).
These simplifying features allow a single button to be configured and used like this:
AceButton button(BUTTON_PIN);void setup() { pinMode(BUTTON_PIN, INPUT_PULLUP); button.setEventHandler(handleEvent); ... }
void loop() { button.check(); }
void handleEvent(AceButton* button, uint8_t eventType, uint8_t buttonState) { ... }
To configure the System ButtonConfig, you may need to add something like this to the
setup()section:
button.getButtonConfig()->setFeature(ButtonConfig::kFeatureLongPress);
When transitioning from a single button to multiple buttons, it's important to remember what's happening underneath the convenience methods. The single
AceButtonbutton is assigned to the System ButtonConfig that was created automatically. When an
EventHandleris assigned to the button, it is actually assigned to the System ButtonConfig. All subsequent instances of
AceButtonwill also be associated with this event handler, unless another
ButtonConfigis explicitly assigned.
There are at least 2 ways you can configure multiple buttons.
Option 1: Multiple ButtonConfigs
#include using namespace ace_button;ButtonConfig config1; AceButton button1(&config1); ButtonConfig config2; AceButton button2(&config2);
void button1Handler(AceButton* button, uint8_t eventType, uint8_t buttonState) { Serial.println("button1"); }
void button2Handler(AceButton* button, uint8_t eventType, uint8_t buttonState) { Serial.println("button2"); }
void setup() { Serial.begin(115200); pinMode(6, INPUT_PULLUP); pinMode(7, INPUT_PULLUP); config1.setEventHandler(button1Handler); config2.setEventHandler(button2Handler); button1.init(6); button2.init(7); }
void loop() { button1.check(); button2.check(); } </acebutton.h>
See the example sketch TwoButtonsUsingTwoButtonConfigs.ino which uses 2
ButtonConfiginstances to configure 2
AceButtoninstances.
Option 2: Multiple Button Discriminators
Another technique keeps the single system
ButtonConfigand the single
EventHandler, but use the
AceButton::getPin()to discriminate between the multiple buttons:
#include using namespace ace_button;AceButton button1(6); AceButton button2(7);
void button1Handler(AceButton* button, uint8_t eventType, uint8_t buttonState) { Serial.println("button1"); }
void button2Handler(AceButton* button, uint8_t eventType, uint8_t buttonState) { Serial.println("button2"); }
void buttonHandler(AceButton* button, uint8_t eventType, uint8_t buttonState) { switch (button->getPin()) { case 6: button1Handler(button, eventType, buttonState); break; case 7: button2Handler(button, eventType, buttonState); break; } }
void setup() { Serial.begin(115200); pinMode(6, INPUT_PULLUP); pinMode(7, INPUT_PULLUP); ButtonConfig* config = ButtonConfig::getSystemButtonConfig(); config->setEventHandler(buttonHandler); }
void loop() { button1.check(); button2.check(); } </acebutton.h>
See the example code TwoButtonsUsingOneButtonConfig. which uses a single
ButtonConfiginstance to handle 2
AceButtoninstances.
Sometimes, it is more convenient to use the
AceButton::getId()method to identify the button instead of the
AceButton::getPin(). See ArrayButtons.ino for an example.
The
EventHandleris a typedef that is defined to be a function pointer. This is a simple, low-overhead design that produces the smallest memory footprint, and allows the event handler to be written with the smallest amount of boilerplate code. The user does not have to override a class.
In more complex applications involving larger number of
AceButtonand
ButtonConfigobjects, it is often useful for the
EventHandlerto be an object instead of a simple function pointer. This is especially true if the application uses Object Oriented Programming (OOP) techniques for modularity and encapsulation. Using an object as the event handler allows additional context information to be injected into the event handler.
To support OOP techniques, AceButton v1.6 adds:
IEventHandlerinterface class
handleEvent()
ButtonConfig::setIEventHandler()method
IEventHandlerinterface.
The
IEventHandlerinterface is simply this:
C++ class IEventHandler { public: virtual void handleEvent(AceButton* button, uint8_t eventType, uint8_t buttonState) = 0; };
At least one of
ButtonConfig::setEventHandler()or
ButtonConfig::setIEventHandler()must be called before events are actually dispatched. If both are called, the last one takes precedence.
See examples/SingleButtonUsingIEventHandler for an example.
On a project using only a small number of buttons (due to physical limits or the limited availability of pins), it may be desirable to distinguish between a single Clicked event and a DoubleClicked event from a single button. This is a challenging problem to solve because fundamentally, a DoubleClicked event must always generate a Clicked event, because a Clicked event must happen before it can become a DoubleClicked event.
Notice that on a desktop computer (running Windows, MacOS or Linux), a double-click on a mouse always generates both a Clicked and a DoubleClicked. The first Click selects the given desktop object (e.g. an icon or a window), and the DoubleClick performs some action on the selected object (e.g. open the icon, or resize the window).
The AceButton Library provides 3 solutions which may work for some projects:
Method 1: The
kFeatureSuppressClickBeforeDoubleClickflag causes the first Clicked event to be detected, but the posting of the event message (i.e. the call to the
EventHandler) is postponed until the state of the DoubleClicked can be determined. If the DoubleClicked happens, then the first Clicked event message is suppressed. If DoubleClicked does not occur, the long delayed Clicked message is sent via the
EventHandler.
There are two noticeable disadvantages of this method. First, the response time of all Clicked events is delayed by about 600 ms (
kClickDelay + kDoubleClickDelay) whether or not the DoubleClicked event happens. Second, the user may not be able to accurately produce a Clicked event (due to the physical characteristics of the button, or the user's dexterity).
It may also be worth noting that only the Clicked event is postponed. The accompanying Released event of the Clicked event is not postponed. So a single click action (without a DoubleClick) produces the following sequence of events to the EventHandler:
kEventPressed- at time 0ms
kEventReleased- at time 200ms
kEventClicked- at time 600ms (200ms + 400ms)
The
ButtonConfigconfiguration looks like this:
C++ ButtonConfig* buttonConfig = button.getButtonConfig(); buttonConfig->setFeature(ButtonConfig::kFeatureDoubleClick); buttonConfig->setFeature( ButtonConfig::kFeatureSuppressClickBeforeDoubleClick);
See the example code at
examples/ClickVersusDoubleClickUsingSuppression/.
Method 2: A viable alternative is to use the Released event instead of the Clicked event to distinguish it from the DoubleClicked. For this method to work, we need to suppress the Released event after both Clicked and DoubleClicked.
The advantage of using this method is that there is no response time lag in the handling of the Released event. To the user, there is almost no difference between triggering on the Released event, versus triggering on the Clicked event.
The disadvantage of this method is that the Clicked event must be be ignored (because of the spurious Clicked event generated by the DoubleClicked). If the user accidentally presses and releases the button too quickly, it generates a Clicked event, which will cause the program to do nothing.
The
ButtonConfigconfiguration looks like this:
C++ ButtonConfig* buttonConfig = button.getButtonConfig(); buttonConfig->setEventHandler(handleEvent); buttonConfig->setFeature(ButtonConfig::kFeatureDoubleClick); buttonConfig->setFeature(ButtonConfig::kFeatureSuppressAfterClick); buttonConfig->setFeature(ButtonConfig::kFeatureSuppressAfterDoubleClick);
See the example code at
examples/ClickVersusDoubleClickUsingReleased/.
Method 3: We could actually combine both Methods 1 and 2 so that either Released or a delayed Click is considered to be a "Click". This may be the best of both worlds.
The
ButtonConfigconfiguration looks like this:
C++ ButtonConfig* buttonConfig = button.getButtonConfig(); buttonConfig->setEventHandler(handleEvent); buttonConfig->setFeature(ButtonConfig::kFeatureDoubleClick); buttonConfig->setFeature( ButtonConfig::kFeatureSuppressClickBeforeDoubleClick); buttonConfig->setFeature(ButtonConfig::kFeatureSuppressAfterClick); buttonConfig->setFeature(ButtonConfig::kFeatureSuppressAfterDoubleClick);
See the example code at
examples/ClickVersusDoubleClickUsingBoth/.
Sometimes it is useful to capture both a Pressed event and a LongPressed event from a single button. Since every button press always triggers a
kEventPressedevent, the only reasonable way to distinguish between Pressed and LongPressed is to use the
kEventReleasedas a substitute for the simple Pressed event. When we active
kFeatureLongPress, we then must activate the
kFeatureSuppressAfterLongPressfeature to suppress the
kEventReleasedevent after the
kEventLongPressedto avoid yet another overlap of events.
ButtonConfig* config = button.getButtonConfig(); config->setFeature(ButtonConfig::kFeatureLongPress); config->setFeature(ButtonConfig::kFeatureSuppressAfterLongPress);
This works most of the time, but I encountered an edge case. Occasionally we want to capture the Released event after the LongPressed event, even if
kEventReleasedmust be suppressed as described above. To solve this edge case, in v1.8, I added a new event type
kEventLongReleasedwhich is triggered as a substitute for
kEventReleased, only if
kFeatureSuppressAfterLongPressis used to suppress
kEventReleased.
See the example code at examples/PressVersusLongPress to see how all these come together.
A number of edge cases occur when the microcontroller is rebooted:
ButtonConfigis configured to support RepeatPress events, should the
kEventRepeatPressedevents be triggered initially?
I think most users would expect that in all these cases, the answer is no, the microcontroller should not trigger an event until the button undergoes a human-initiated change in state. The AceButton library implements this logic. (It might be useful to make this configurable using a
ButtonConfigfeature flag but that is not implemented.)
On the other hand, it is sometimes useful to perform some special action if a button is pressed while the device is rebooted. To support this use-case, call the
AceButton::isPressedRaw()in the global
setup()method (after the button is configured). It will directly call the
digitalRead()method associated with the button pin and return
trueif the button is in the Pressed state.
When a Clicked event is generated, the
AceButtonclass looks for a second Clicked event within a certain time delay (default 400 ms) to determine if the second Clicked event is actually a DoubleClicked event.
All internal timestamps in
AceButtonare stored as
uint16_t(i.e. an unsigned integer of 16 bits) in millisecond units. A 16-bit unsigned counter rolls over after 65536 iterations. Therefore, if the second Clicked event happens between (65.636 seconds, 66.036 seconds) after the first Clicked event, a naive-logic would erroneously consider the (long-delayed) second click as a double-click.
The
AceButtoncontains code that prevents this from happening.
Note that even if the
AceButtonclass uses an
unsigned longtype (a 32-bit integer on the Arduino), the overflow problem would still occur after
2^32milliseconds (i.e. 49.7 days). To be strictly correct, the
AceButtonclass would still need logic to take care of orphaned Clicked events.
Instead of allocating one pin for each button, we can use Binary Encoding to support large number of buttons with only a few pins. The circuit can be implemented using a 74LS148 chip, or simple diodes like this:
Three subclasses of
ButtonConfigare provided to handle binary encoded buttons:
Encoded4To2ButtonConfig: 3 buttons with 2 pins
Encoded8To3ButtonConfig: 7 buttons with 3 pins
EncodedButtonConfig:
M=2^N-1buttons with
Npins
See docs/binary_encoding/README.md for information on how to use these classes.
It is possible to attach 5-8 (maybe more) buttons on a single analog pin through a resistor ladder, and use the
analogRead()to read the different voltages generated by each button. An example circuit looks like this:
The
LadderButtonConfigclass handles this configuration.
See docs/resistor_ladder/README.md for information on how to use this class.
All classes in this library were originally designed to be created statically at startup time and never deleted during the lifetime of the application. Since they were never meant to be deleted through the pointer, I did not include the virtual destructor for polymorphic classes (i.e.
ButtonConfigand its subclasses). The
AceButtonclass is not polymorphic and does not need a virtual destructor.
Most 8-bit processors have limited flash and static memory (for example, 32 kB flash and 2 KB static for the Nano or UNO). Adding a virtual destructor causes 600 additional bytes of flash memory to be consumed. I suspect this is due to the virtual destructor pulling the
malloc()and
free()functions which are needed to implement the
newand
deleteoperators. For a library that consumes only about 1200 bytes on an 8-bit processor, this increase in flash memory size did not seem acceptable.
For 32-bit processors (e.g. ESP8266, ESP32) which have far more flash memory (e.g. 1 MB) and static memory (e.g. 80 kB), it seems reasonable to allow
AceButtonand
ButtonConfigto be created and deleted from the heap. (See Issue #46 for the motivation.) Testing shows that the virtual destructor adds only about 60-120 bytes of flash memory for these microcontrollers, probably because the
malloc()and
free()functions are already pulled in by something else. The 60-120 bytes of additional consumption seems trivial compared to the range of ~256 kB to ~4 MB flash memory available on these 32-bit processors.
Therefore, I added a virtual destructor for the
ButtonConfigclass (v1.5) and enabled it for all architectures other than
ARDUINO_ARCH_AVR(v1.6.1). This prevents 8-bit processors with limited memory from suffering the overhead of an extra 600 bytes of flash memory usage.
Even for 32-bit processors, I still recommend avoiding the creation and deletion of objects from the heap, to avoid the risk of heap fragmentation. If a variable number of buttons is needed, it might be possible to design the application so that all buttons which will ever be needed are predefined in a global pool. Even if some of the
AceButtonand
ButtonConfiginstances are unused, the overhead is probably smaller than the overhead of wasted space due to heap fragmentation.
Here are the sizes of the various classes on the 8-bit AVR microcontrollers (Arduino Uno, Nano, etc):
and 32-bit microcontrollers:
(An early version of
AceButton, with only half of the functionality, consumed 40 bytes. It got down to 11 bytes before additional functionality increased it to 14.)
Program size:
MemoryBenchmark was used to determine the size of the library for various microcontrollers (Arduino Nano to ESP32). Here are some ranges of number to give a rough estimate of how much flash and static memory are consumed for various button configurations:
ButtonConfig
Encoded4To2ButtonConfig
Encoded8To3ButtonConfig
EncodedButtonConfig
LadderButtonConfig
CPU cycles:
The profiling numbers for
AceButton::check()using a simple
ButtonConfigcan be found in examples/AutoBenchmark.
In summary, the average numbers for various boards are:
If you use the more advanced
EncodedButtonConfigor
LadderButtonConfigto check more buttons, each iteration through all the buttons takes longer. As a rough summary, to check 7 buttons:
analogRead()function on a SAMD21 is significantly slower than other microcontrollers. I recommend double-checking these numbers.
The library has been extensively tested on the following boards:
I will occasionally test on the following boards as a sanity check:
This library was developed and tested using:
It should work with PlatformIO but I have not tested it.
The library works on Linux or MacOS (using both g++ and clang++ compilers) using the EpoxyDuino emulation layer.
I use Ubuntu Linux 18.04 and 20.04 for most of my development.
There are numerous "button" libraries out there for the Arduino. Why write another one? I wanted to add a button to an addressable strip LED controller, which was being refreshed at 120 Hz. I had a number of requirements:
Since the LED refresh code needed to run while the button code was waiting for a "LongPress" delay, it seemed that the cleanest API for a button library would use an event handler callback mechanism. This reduced the number of candidate libraries to a handful. Of these, only a few of them supported a LongPress event. I did not find the remaining ones flexible enough for my button needs in the future. Finally, I knew that it was tricky to write correct code for debouncing and detecting various events (e.g. DoubleClick, LongPress, RepeatPress). I looked for a library that contained unit tests, and I found none.
I decided to write my own and use the opportunity to learn how to create and publish an Arduino library.
An Arduino UNO or Nano has 16 times more flash memory (32KB) than static memory (2KB), so the library is optimized to minimize the static memory usage. The AceButton library is not optimized to create a small program size (i.e. flash memory), or for small CPU cycles (i.e. high execution speed). I assumed that if you are seriously optimizing for program size or CPU cycles, you will probably want to write everything yourself from scratch.
That said, examples/MemoryBenchmark shows that the library consumes between 970-2180 bytes of flash memory, and AutoBenchmark shows that
AceButton::check()takes between 13-15 microseconds on a 16MHz ATmega328P chip and 2-3 microseconds on an ESP32. Hopefully that is small enough and fast enough for the vast majority of people.
I changed to the MIT License starting with version 1.1 because the MIT License is so simple to understand. I could not be sure that I understood what the Apache License 2.0 meant.
If you find this library useful, consider starring this project on GitHub. The stars will let me prioritize the more popular libraries over the less popular ones.
If you have any questions, comments, bug reports, or feature requests, please file a GitHub ticket instead of emailing me unless the content is sensitive. (The problem with email is that I cannot reference the email conversation when other people ask similar questions later.) I'd love to hear about how this software and its documentation can be improved. I can't promise that I will incorporate everything, but I will give your ideas serious consideration.
Created by Brian T. Park ([email protected]).