Timers
In this section, we will walk through the button_abort
example in Rust. It
uses the first button and the first LED of the board. On a short press, the LED
will start blinking. On a long press, the LED will stop blinking. While the
button is pressed, the LED indicates whether the press is short or long:
- The LED is on while the press is short.
- The LED turns off once the press is long.
This applet will need a shared state to know whether the LED must be blinking or
not. We cannot simply use a boolean because the state will be shared. We cannot
use Cell<bool>
neither because the state must be in the heap1. So we use
Rc<Cell<bool>>
which is a common pattern when using callbacks:
// We define a shared state to decide whether we must blink.
let blinking = Rc::new(Cell::new(false));
We can now allocate a timer for the blinking behavior using timer::Timer::new
.
This function takes the handler that will be called each time the timer fires.
The handler simply toggles the LED if we must be blinking. Note how we must move
a clone of the state to the callback. This is also a common pattern when using
callbacks, because callbacks must be 'static
1:
// Allocate a timer for blinking the LED.
let blink = timer::Timer::new({
// Move a clone of the state to the callback.
let blinking = blinking.clone();
move || {
// Toggle the LED if blinking.
if blinking.get() {
led::set(0, !led::get(0));
}
}
});
The rest of the code is done in an infinite loop:
loop {
At each iteration, we start by setting up a button::Listener
to record whether
the button was pressed and then released. The logic is similar to the callback
setup for the timer. The small difference is that we won't need to call any
function on the listener so we prefix its variable name _button
with an
underscore. We cannot simply omit the variable name because we don't want to
drop it until the end of the loop iteration, otherwise we would stop listening
to button events.
// Setup button listeners.
let pressed = Rc::new(Cell::new(false));
let released = Rc::new(Cell::new(false));
let _button = button::Listener::new(0, {
let pressed = pressed.clone();
let released = released.clone();
move |state| match state {
button::Pressed => pressed.set(true),
button::Released if pressed.get() => released.set(true),
button::Released => (),
}
})?;
We then wait until the button is pressed using scheduling::wait_until()
. This
function takes a condition as argument and only executes callbacks until the
condition is satisfied.
// Wait for the button to be pressed.
scheduling::wait_until(|| pressed.get());
According to the specification of this example applet, when the button is
pressed we must turn on the LED (and stop blinking if we were blinking) to
signal a possible short press. Note that callbacks can only executed when a
scheduling function is called, so we are essentially in a critical section. As
such, the order in which we do those 3 lines doesn't matter. However, a callback
might be scheduled before we stop the blink
timer. It will execute next time
we call a scheduling function. This is ok because when that will happen, the
blinking
state will be false
and the blink
handler will do nothing.
// Turn the LED on.
blink.stop();
blinking.set(false);
led::set(0, led::On);
To detect a long press, we need to start a timer. There is nothing new here
except the Timer::start()
function which takes the timer mode (one-shot or
periodic) and its duration.
// Start the timeout to decide between short and long press.
let timed_out = Rc::new(Cell::new(false));
let timer = timer::Timer::new({
let timed_out = timed_out.clone();
move || timed_out.set(true)
});
timer.start(timer::Oneshot, Duration::from_secs(1));
We now wait for the first event between the button being released and the timeout firing. This is simply done by using a condition which is a disjunction of the events of interest. This is a common pattern when implementing behavior with a timeout.
// Wait for the button to be released or the timeout to fire.
scheduling::wait_until(|| released.get() || timed_out.get());
To signal that the timeout was reached or the button was released, we turn off the LED.
// Turn the LED off.
led::set(0, led::Off);
Finally, if the press was short (i.e. the button was released before the timeout), we start blinking. This demonstrates the use of periodic timers.
// Start blinking if short press.
if !timed_out.get() {
blinking.set(true);
blink.start(timer::Periodic, Duration::from_millis(200));
}
There are a few things to note:
- The code is implicit in Rust, but the button handler and the timer handler
within the loop iteration are dropped before the next iteration. This means
that their callbacks are unregistered. This could be done explicitly by
calling
core::mem::drop()
on their variable if needed. - It is not needed to start and stop the
blink
timer within the loop as long as it is started before entering the loop. This is just an optimization to avoid calling the handler when we know that theblinking
shared state isfalse
, because the handler would do nothing in that case.
The final code looks like this:
#![no_std] wasefire::applet!(); use alloc::rc::Rc; use core::cell::Cell; use core::time::Duration; fn main() -> Result<(), Error> { assert!(button::count() > 0, "Board has no buttons."); assert!(led::count() > 0, "Board has no LEDs."); // We define a shared state to decide whether we must blink. let blinking = Rc::new(Cell::new(false)); // Allocate a timer for blinking the LED. let blink = timer::Timer::new({ // Move a clone of the state to the callback. let blinking = blinking.clone(); move || { // Toggle the LED if blinking. if blinking.get() { led::set(0, !led::get(0)); } } }); loop { // Setup button listeners. let pressed = Rc::new(Cell::new(false)); let released = Rc::new(Cell::new(false)); let _button = button::Listener::new(0, { let pressed = pressed.clone(); let released = released.clone(); move |state| match state { button::Pressed => pressed.set(true), button::Released if pressed.get() => released.set(true), button::Released => (), } })?; // Wait for the button to be pressed. scheduling::wait_until(|| pressed.get()); // Turn the LED on. blink.stop(); blinking.set(false); led::set(0, led::On); // Start the timeout to decide between short and long press. let timed_out = Rc::new(Cell::new(false)); let timer = timer::Timer::new({ let timed_out = timed_out.clone(); move || timed_out.set(true) }); timer.start(timer::Oneshot, Duration::from_secs(1)); // Wait for the button to be released or the timeout to fire. scheduling::wait_until(|| released.get() || timed_out.get()); // Turn the LED off. led::set(0, led::Off); // Start blinking if short press. if !timed_out.get() { blinking.set(true); blink.start(timer::Periodic, Duration::from_millis(200)); } } }
Testing
As for the LEDs and buttons examples, to test the applet on the host
runner,
you'll need to use:
cargo xtask applet rust button_abort runner host --log=info
However, in addition to button
which does a press and release sequence, you
can use press
and release
to independently press and release the button. In
particular, button
may be used to start blinking and press
may be used to
stop blinking. There's no need to explicitly release because the applet supports
missing callbacks for robustness.
If the state were on the stack and a callback were pointing to that
state, it would become a safety requirement to unregister the callback
before popping the state from the stack. However, it is safe to leak a
callback with core::mem::forget()
and thus not drop it. So we enforce
callbacks to be 'static
and thus not depend on references to the stack.