Managing Events
In interactive systems, many interesting actions happen at specific moments in time: a performer presses a button, a sensor crosses a threshold, a timer runs out, a metronome ticks. These fleeting moments are distinct from continuous signals flows like the brightness of a light or the position of a knob: they happen in an instant and are then gone.
In Plaquette, an event is an instantaneous condition that occurs on a unit for exactly one step: the push of a button, the tick of a metronome, the end of a timer, or a peak detection. Plaquette offers two different and complementary ways to work with events: trigger conditions and callback functions.
Trigger conditions are true for the single step when the event occurs. They are typically
polled continuously inside the step() function using if statements. We have already been
working with some of them in the Inputs and Outputs and Working with Time sections:
void step() { if (button.rose()) { ... } // true only on the step when button is pressed if (metro) { ... } // true only on the step when the metronome ticks }
Callback functions are functions that you register once in begin(). They are called
automatically when the event occurs, without needing to write step() logic.
void doSomething() { ... } // user-defined function void begin() { button.onRise(doSomething); // function doSomething() will be called on button press metro.onBang(doSomething); // same, but triggered by metronome tick }
Note
Registering a callback function is like giving someone instructions on what to do when a specific event happens. For example, imagine you are baking cookies and you set a timer. Instead of constantly watching the oven, you set the timer to “call you back” (in other words, alert you) when time is up, so you can take the cookies out.
Use trigger conditions when you have few events to manage and want to keep things simple: all your
logic stays in step(), easy to read top-to-bottom, and you can combine events with other conditions
(e.g. if (button.rose() && sensor > 0.7)).
Use callback functions when you have many events to manage or want to reuse the same action across
different events: register them once in begin() and they will be called automatically so you don’t
have to think about polling them.
Supported Events
This table lists all supported events, their trigger condition and callback form, and which units support them.
When |
Trigger |
Callback |
Units |
|---|---|---|---|
Unit emits a pulse |
unit itself |
|
|
Digital value toggles (on/off) |
|
|
|
Digital value goes from on to off |
|
|
|
Timed process completes |
|
|
|
Wave completes a full cycle |
|
|
|
Wave passes its skew point |
|
|
|
Digital value goes from off to on |
|
|
|
New data is available |
|
|
Using Trigger Conditions
Trigger functions are the simplest way to react to events. Call them inside step()
with an if statement:
void step() {
if (button.rose()) // button was just pressed
led.toggle();
if (alarm.finished()) // alarm just timed out
println("Time is up!");
if (metro) // metronome ticked
println("Tick...");
}
Since triggers are only true for one step, they naturally detect transitions rather than
ongoing states. You can combine them with logical operators and other conditions to create
more complex behaviors:
void step() {
// Only react to button press when sensor value is high enough.
if (button.rose() && sensor > 0.7)
led.toggle();
// React to either button press or metronome tick.
if (button.rose() || metro)
println("Something happened!");
}
Warning
Since triggers are true for only one step, they must be checked at every call to ``step()``
or they will be missed. A common mistake comes when nesting a trigger inside another trigger’s if block:
void step() {
// WRONG: button.rose() is only checked when metro fires,
// so most button presses will be missed
if (metro) {
if (button.rose())
led.toggle();
}
}
In this example, button.rose() is only evaluated on the rare step when metro is true.
On all other steps, the button press goes undetected. Instead, check both triggers independently.
Using Callback Functions
As your project grows and you start managing more events, the step() function can become
cluttered with if statements, making event management confusing and error-prone. Callback functions offer
a different approach: instead of checking for events yourself, you tell each unit what to do when an
event happens, and Plaquette automatically takes care of the rest. This keeps your code organized and
ensures no event is ever missed.
Let us take an example where we want to react to the push of a button by switching an LED on and off.
First, let us create the units we will be working with:
#include <Plaquette.h>
DigitalOut led(LED_BUILTIN); // LED connected to built-in pin
DigitalIn button(2, INTERNAL_PULLUP); // Button connected to pin 2
In order to react to an event, we first need to create a callback function which will be called when the event will happen:
// Callback function to toggle the LED.
void toggleLed() {
led.toggle();
}
Then, we need to register our callback to an event. In this case, we will register our function toggleLed()
to the onRise() event of our button unit, which will trigger at the instant the button is pressed.
void begin() {
button.debounce(); // Enable debouncing to avoid multiple events
// Register callbacks for button events.
button.onRise(toggleLed); // Toggle the LED on button press
}
In this case, since the callback will take care of all the logic, we do not even need to declare a step()
function!
Here is the final code for this example:
#include <Plaquette.h>
DigitalOut led(LED_BUILTIN); // LED connected to built-in pin
DigitalIn button(2, INTERNAL_PULLUP); // Button connected to pin 2
// Callback function to toggle the LED.
void toggleLed() {
led.toggle();
}
void begin() {
button.debounce(); // Enable debouncing to avoid multiple events
// Register callbacks for button events.
button.onRise(toggleLed); // Toggle the LED on button press
}
// Notice that we haven't invoked step(){} because it isn't necessary
Now, try changing onRise() to onFall() or to onChange(). How does that affect the interaction
between the button and the LED?
Managing Multiple Events
It is possible to register multiple callbacks with the same event. Likewise, a single callback can be registered with many events.
Example: Launch both toggleLed() and printButton() on button press, registering printButton() to both
press and release events.
#include <Plaquette.h>
DigitalOut led(LED_BUILTIN); // LED connected to built-in pin
DigitalIn button(2, INTERNAL_PULLUP); // Button connected to pin 2
Monitor monitor(115200); // Serial monitor.
// Callback function to toggle the LED.
void toggleLed() {
led.toggle();
}
// Callback function to print button state.
void printButton() {
print("Button ");
println(button ? "pressed" : "released");
}
void begin() {
button.debounce(); // Enable debouncing to avoid multiple events
// Register callbacks for button events.
button.onRise(toggleLed); // Toggle the LED on button press
button.onRise(printButton); // Print button state
button.onFall(printButton); // Same here
}
Coordinating Parallel Events with Metronomes
There are many applications for which things happen concurrently at different pace, making
one wish there could be multiple looping functions similar to step() running in parallel at different
rates. This is easy to achieve in Plaquette using event-driven coding. Metronomes tick at a specific
period, generating “bang” events which can trigger callbacks by registering them to the onBang() event.
In this example, two metronomes control two LEDs, one digital and one analog, each at a different interval. A ramp is used to fade the analog LED.
#include <Plaquette.h>
DigitalOut led1(LED_BUILTIN); // First LED (digital) connected to built-in pin
AnalogOut led2(9); // Second LED (PWM) connected to pin 9
Metronome metro1(1.0); // Metronome with a 1 second period
Metronome metro2(2.0); // Metronome with a 2 seconds period
Ramp rampLed(0.5); // Short ramp to control LED 2
// Function to toggle the first LED.
void pingLed1() {
led1.toggle();
}
// Function to start the ramp on second LED.
void pingLed2() {
rampLed.start();
}
void begin() {
// Register callbacks for the metronomes.
metro1.onBang(pingLed1); // Toggle LED 1 every second
metro2.onBang(pingLed2); // Fade in LED 2 every 2 seconds
}
void step() {
rampLed >> led2; // Ramp second LED from 100% to 0%
}
Creating On-the-fly Callbacks
For simple, localized actions, you can define callback functions directly inline using an anonymous function (also called lambda function) which can be created with the following syntax:
[]() {
// Function content goes here.
}
It allows you to write concise code without defining separate named functions and is thus especially useful for short, self-contained actions, keeping the code clean and readable.
For example, we could rewrite the callback registration from the example above in a shorter way, like this:
void begin() {
// Register callbacks for the metronomes.
metro1.onBang([]() { led1.toggle(); }); // Toggle LED 1 every second
metro2.onBang([]() { rampLed.start(); }); // Fade in LED 2 every 2 seconds
}
Conclusion
Event-driven programming in Plaquette simplifies the process of reacting to changes and scheduling actions, allowing you to write modular, expressive, and efficient code. By using callbacks and event sources like buttons and metronomes, you can manage complex behaviors that happen concurrently and at different rates.