In most projects, the goal is usually to 'get my idea working'™ rather than messing around in the weeds with low level stuff. This is where libraries like Thomas Fredericks' Bounce2 and Paul Stoffregen's Encoder have helped (me) enormously over the years. People far cleverer than I have written code that does its job very reliably.
Working with physical inputs involves 'listening' to one or more pins and acting on changes to the state of those. For inputs like buttons and encoders, state changes over a time also play a role, so things can get quite messy, quite quickly. If you have multiple inputs, that can soon become Spaghetti code.
This tutorial will show how using libraries (well, a library) can free you to concentrate on the code that matters to you. This library is called InputEvents and is an evolution of a few libraries I wrote to get me out of the corners I coded myself into when using Bounce2 and Encoder on their own. If you are new to programming, it will also introduce the Arduino-style 'event programming' concept.
As noted in the comments below, there are many ways (and other libraries) that can be used to read physical inputs but using event is probably the cleanest way to deal with input from users & pins. The InputEvents library provides a consistent approach to all the different input types it supports.
While your primary project goal is to get your idea working, the overriding aim of good coding should be maintainability. You want to be able to add features to your code without breaking existing features. You want to be able to find and fix bugs without introducing new ones (aka regressions). To this end, we will follow the Unix mantra: "Do one thing but do it well".
This means extracting everything possible from your loop() function and putting it into one or (likely) more functions that each do one thing well. The functions are then called by loop() or by another function. In the Arduino world, separating things into functions was often less common because of CPU/memory restrictions on early microcontrollers but as we now usually have much more available RAM and flash storage, we have the opportunity to create more maintainable code for our projects.
Note: Throughout this tutorial, you can substitute 'function' for 'class method' but that is a whole other subject. 'method' is just the name for a 'function' within a class.
Here is a really basic example of extracting code from loop() into a function:
void doMyThing() {
Serial.println("Doing my thing");
}
void loop() {
doMyThing();
}
Admittedly, it is a little silly but if you have a bug when 'Doing my thing', you know exactly where to look and any changes you make will only affect that functionality.
Now imagine you have a few physical inputs on your board. We want to 'encapsulate' what functionality each input performs in a function. Nice. Clean. Code.
Here is a function that might do some things if a button 'event' occurs (an event could be 'pressed', 'released', 'clicked' and a few more).
void onButtonEvent(InputEventType et, EventButton& eb) {
// The logic/code for your button events goes here
Serial.println("A button event happened");
}
The function now takes two parameters (which together with its return value void, defines the function 'signature'). The first parameter named et is the event type and the second ebis a reference to the EventButton that is 'connected' to the pin. From the et parameter we can decide what to do and from the eb parameter we can read the state of the button - such as how many clicks were counted. I have used the name eb here but it is a reference to (ie the same as) the myButton created below.
Note: the term 'parameter' and 'argument' are often used interchangeably - a parameter is part of what defines a function signature, an argument is what is passed to to a function parameter. See Difference Between Parameters and Arguments - GeeksforGeeks for clarification.
The function name and parameter names can be anything you like (descriptive is best) so long as the function has the correct signature. The prefix on... is just a convention. Often the suffix ...Handler is used instead, ie myButtonEventHandler. et and eb are not great examples of descriptive naming but it is generally accepted, the smaller the scope, the smaller the name can be!
We need to 'connect' the EventButton to a pin, so we must create an 'instance' of EventButton:
EventButton myButton(4);
The constructor's parameter receives the number of the pin your button is physically attached to (in this example, 4). A constructor is just a special method (a class 'function' with the same name as the class) that is 'called' when an instance of that class is created.
In our setup() function we must initialise the button wit begin() and tell the button to use our onButtonEvent() function:
void setup() {
myButton.begin();
myButton.setCallback(onButtonEvent);
and just like magic, your onButtonEvent function has become a 'callback' or a 'handler'. These are just terms to describe a function that is (usually) called when an event happens.
In larger systems (PCs, servers etc) that event is often 'fired' from a separate process or thread but most microcontrollers only have one process/thread so we fake it in our loop() function:
void loop() {
myButton.update();
}
The EventButton's update() method will check the state of the button pin and decide if it is appropriate to 'fire' an event. If it is, then your onButtonEvent function will be called (back) and the code within your function will be executed.
With a function and four lines of code, you no longer have to worry about what the details of reading inputs, freeing you to concentrate on what you want to do if they are pressed, turned, moved etc.
This is a core tenet of good programming - separating concerns.
An example sketch for toggling an LED on an Arduino is here: ButtonToggle and can be seen in action on Wokwi here.
The EventInputs library can be used with Buttons, Encoders, Encoder Buttons (my favourite
), Potentiometers (Analog inputs), Joysticks and Switches. The inputs don't have to be physical but that is the primary expected use case. It has been tested on all the Arduinos I own, plus ESP8266, ESP32 and Teensy 4.1.
