Codelab#
pw_async2: Cooperative async tasks for embedded
Welcome to the pw_async2
codelab!
This codelab provides a hands-on introduction to Pigweed’s cooperative
asynchronous framework. You will build a simple, simulated “Vending Machine”
application — an automated machine that asynchronously waits for user input
like coin insertion and keypad presses before dispensing an item. Along the
way, you’ll learn the core concepts of pw_async2
.
By the end of this codelab, you will know how to:
Implement a
pw::async2::Task
as a state machine.Call asynchronous functions and manage state across suspension points.
Write your own pendable functions that use a
Waker
to handle external events.Use
pw::async2::OnceSender
andpw::async2::OnceReceiver
for basic inter-task communication.Use
pw::async2::TimeProvider
andpw::async2::Select
to implement timeouts.
Let’s get started!
Setup#
The code for this codelab is part of the Pigweed repository. If you haven’t already, follow the contributor guide to clone the Pigweed repository and set up your development environment.
Tip
We encourage you to implement each step on your own, but if you ever get stuck, a solution is provided at the start of each step.
Step 1: Hello, Async World!#
The first step is to create and run a basic asynchronous task. This will
introduce you to the two most fundamental components of pw_async2
: the
Task and the Dispatcher.
Solution for this step
What’s a Task?#
A Task is the basic unit of execution in this framework. It’s an object that represents a job to be done, like blinking an LED, processing sensor data, or, in our case, running a vending machine.
Tasks are implemented by inheriting from the pw::async2::Task
class and
implementing a single virtual method: DoPend()
. This method is where the
task’s logic lives.
Let’s look at the code.
You’ll see the definition for our VendingMachineTask
:
1#pragma once
2
3#include "pw_async2/task.h"
4
5namespace codelab {
6
7// The main task that drives the vending machine.
8class VendingMachineTask : public pw::async2::Task {
9 public:
10 VendingMachineTask()
11 : pw::async2::Task(PW_ASYNC_TASK_NAME("VendingMachineTask")) {}
12
13 private:
14 // This is the core of the asynchronous task. The dispatcher calls this method
15 // to give the task a chance to do work.
16 pw::async2::Poll<> DoPend(pw::async2::Context& cx) override;
17};
18
19} // namespace codelab
It’s a simple class that inherits from pw::async2::Task
. The important part
is the DoPend
method, which is where we’ll add our logic.
Here you’ll find the incomplete implementation of DoPend
:
1#include "vending_machine.h"
2
3#include "pw_async2/try.h"
4#include "pw_log/log.h"
5
6namespace codelab {
7
8pw::async2::Poll<> VendingMachineTask::DoPend(pw::async2::Context&) {
9 // Fill in your implementation here.
10 return pw::async2::Ready();
11}
12
13} // namespace codelab
The DoPend
method returns a Poll<>. A
Poll
can be in one of two states:
Ready()
: The task has finished its work.Pending()
: The task is not yet finished and should be run again later.
Our stub currently simply returns Ready()
, meaning it would exit
immediately without doing any work.
What’s a Dispatcher?#
A Dispatcher is the engine that runs the
tasks. It’s a simple, cooperative scheduler. You give it tasks by calling
Post()
, and then you tell it to run them by calling RunUntilStalled()
or RunToCompletion()
.
The dispatcher maintains a queue of tasks that are ready to be polled. When a
run is triggered, it pulls a task from the queue and invokes its DoPend()
method. If the task returns Pending()
, the task is put to sleep until it
is woken by the operation that blocked it. If it returns Ready()
, the
dispatcher considers it complete and will not run it again.
Here you can see we have a dispatcher, but it’s not doing anything yet.
1#include "coin_slot.h"
2#include "hardware.h"
3#include "pw_async2/dispatcher.h"
4#include "vending_machine.h"
5
6namespace {
7
8codelab::CoinSlot coin_slot;
9
10} // namespace
11
12// Interrupt handler function invoked when the user inserts a coin into the
13// vending machine.
14void coin_inserted_isr() { coin_slot.Deposit(); }
15
16// Interrupt handler function invoked when the user presses a key on the
17// machine's keypad. Receives the value of the pressed key (0-9).
18void key_press_isr(int /*key*/) {
19 // In Step 3, implement your keypad handler here.
20}
21
22// Interrupt handler function invoked to simulate the item drop detector
23// detecting confirmation that an item was successfully dispensed from the
24// machine.
25void item_drop_sensor_isr() {
26 // In Step 5 you will uses this as part of a new Dispense task that runs
27 // the dispenser motor until an item drops, or you time out on the vend
28 // operation.
29}
30
31int main() {
32 pw::async2::Dispatcher dispatcher;
33 codelab::HardwareInit(&dispatcher);
34
35 // Fill in your implementation here.
36
37 return 0;
38}
Putting it all together#
Now, let’s modify the code to print a welcome message.
In pw_async2/codelab/vending_machine.cc, change DoPend
to log the
message:
Welcome to the Pigweed Vending Machine!
Keep the Ready()
return, telling the dispatcher to complete the task after
it has logged.
Post and Run the Task
In pw_async2/codelab/main.cc, create an instance of your vending machine task and give it to the dispatcher to run.
1#include "coin_slot.h"
2#include "hardware.h"
3#include "pw_async2/dispatcher.h"
4#include "vending_machine.h"
5
6namespace {
7
8codelab::CoinSlot coin_slot;
9
10} // namespace
11
12// Interrupt handler function invoked when the user inserts a coin into the
13// vending machine.
14void coin_inserted_isr() { coin_slot.Deposit(); }
15
16// Interrupt handler function invoked when the user presses a key on the
17// machine's keypad. Receives the value of the pressed key (0-9).
18void key_press_isr(int /*key*/) {
19 // In Step 3, implement your keypad handler here.
20}
21
22// Interrupt handler function invoked to simulate the item drop detector
23// detecting confirmation that an item was successfully dispensed from the
24// machine.
25void item_drop_sensor_isr() {
26 // In Step 5 you will uses this as part of a new Dispense task that runs
27 // the dispenser motor until an item drops, or you time out on the vend
28 // operation.
29}
30
31int main() {
32 pw::async2::Dispatcher dispatcher;
33 codelab::HardwareInit(&dispatcher);
34
35 codelab::VendingMachineTask task;
36 dispatcher.Post(task);
37
38 dispatcher.RunToCompletion();
39
40 return 0;
41}
Here, dispatcher.Post(task)
adds our task to the dispatcher’s run queue.
dispatcher.RunToCompletion()
tells the dispatcher to run all of its tasks
until they have all returned Ready()
.
6. Build and Run
Now, build and run the codelab target from the root of the Pigweed repository:
bazelisk run //pw_async2/codelab
You should see the following output:
INF Welcome to the Pigweed Vending Machine!
Congratulations! You’ve written and run your first asynchronous task with
pw_async2
. In the next step, you’ll learn how to have your task run
asynchronous operations.
Step 2: Calling an async function#
In the last step, our task ran from start to finish without stopping. Most real-world tasks, however, need to wait for things: a timer to expire, a network packet to arrive, or, in our case, a user to insert a coin.
In pw_async2
, operations that can wait are called pendable functions.
Solution for this step
What’s a Pendable function?#
A pendable function is a function that, like a Task
implementation’s
DoPend
method, takes an async Context and
returns a Poll
of some value. When a task calls a pendable function, it
checks the return value to determine how to proceed.
If it’s
Ready(value)
, the operation is complete, and the task can continue with thevalue
.If it’s
Pending()
, the operation is not yet complete. The task will generally stop and returnPending()
itself, effectively “sleeping” until it is woken up.
When a task is sleeping, it doesn’t consume any CPU cycles. The Dispatcher
simply won’t poll it again until an external event wakes it up. This is the core
of cooperative multitasking.
For this step, we’ve provided a CoinSlot
class with a pendable function to
read the number of coins inserted:
pw::async2::Poll<unsigned> Pend(pw::async2::Context& cx)
. Let’s use it.
1. Add a CoinSlot
to the vending machine#
First, open pw_async2/codelab/vending_machine.h. You’ll need to include
coin_slot.h
. Add a reference to a CoinSlot
as a member variable of your
VendingMachineTask
and update its constructor to initialize it.
In your pw_async2/codelab/main.cc, we have provided a global CoinSlot
instance. Pass it into your updated task.
2. Wait for a coin#
Now, let’s modify the task’s DoPend
in
pw_async2/codelab/vending_machine.cc. Following your welcome message from
Step 1, prompt the user to insert a coin.
To wait for a coin from the CoinSlot
, you’ll call its Pend
function.
This returns a Poll<unsigned>
indicating the status of the coin slot.
If the
Poll
isPending()
, it means that no coin has been inserted yet. Your task cannot proceed without payment, so it must signal this to the dispatcher by returningPending()
itself. Pendable functions likeCoinSlot::Pend
which wait for data will automatically wake your waiting task once that data becomes available.If the
Poll
isReady()
, it means that coins have been inserted. ThePoll
object now contains the number of coins. Your task can get this value and proceed to the next step.
Here’s how you would write that:
pw::async2::Poll<unsigned> poll_result = coin_slot_.Pend(cx);
if (poll_result.IsPending()) {
return pw::async2::Pending();
}
unsigned coins = poll_result.value();
Add this code to your DoPend
method. After getting the number of coins, log
that a coin was detected and that an item is being dispensed. Finally, return
pw::async2::Ready()
to finish the task.
Go ahead and replace the call to the CoinSlot
in your DoPend
with this
macro. The behavior will be identical, but the code is much cleaner.
3. Build and run: Spot the issue#
Run your vending machine as before:
bazelisk run //pw_async2/codelab
You will see the welcome message, and then the application will wait for your input.
INF Welcome to the Pigweed Vending Machine!
INF Please insert a coin.
To simulate inserting a coin, type c and press Enter in the same terminal. The hardware thread will call the coin slot Interrupt Service Routine (ISR), which wakes up your task. The dispatcher will run it again, and you’ll see… an unexpected result:
INF Welcome to the Pigweed Vending Machine!
INF Please insert a coin.
INF Welcome to the Pigweed Vending Machine!
INF Please insert a coin.
INF Received 1 coin. Dispensing an item.
The welcome message was printed twice! Why?
When a task is suspended and resumed, its DoPend
method is called again
from the beginning. The first time DoPend
ran, it printed the welcome
message and then returned Pending()
from inside the PW_TRY_READY_ASSIGN
macro. When the coin was inserted, the task was woken up and the dispatcher
called DoPend
again from the top. It printed the welcome message a second
time, and then when it called coin_slot_.Pend(cx)
, the coin was available,
so it returned Ready()
and the task completed.
This demonstrates a critical concept of asynchronous programming: tasks must manage their own state.
4. Managing the welcome state#
Because a task can be suspended and resumed at any Pending()
return, you
need a way to remember where you left off. For simple cases like this, a boolean
flag is sufficient.
Open pw_async2/codelab/vending_machine.h and add a boolean to track whether
the welcome message has been displayed. Initialize it to false
.
Now, modify DoPend
in pw_async2/codelab/vending_machine.cc. Gate the
two log calls for the welcome message behind your new boolean flag. Once the
message is printed, make sure to set the flag to true
so it won’t be printed
again.
5. Build and run: Verify the fix#
bazelisk run //pw_async2/codelab
Now, the output should be correct. The welcome message is printed once, the task waits, and then it finishes after you insert a coin.
INF Welcome to the Pigweed Vending Machine!
INF Please insert a coin.
Type c and press Enter:
INF Received 1 coin. Dispensing an item.
The task then completes, RunToCompletion
returns, and the program exits.
You’ve now implemented a task that can wait for an asynchronous event and correctly manages its state! In the next step, you’ll learn how to write your own pendable functions.
Step 3: Writing your own event handler#
In the last step, you created a task to dispense an item after a coin was inserted. Most vending machines at least allow you to choose what to buy. Let’s fix that by handling the keypad input ISR, and using the key press info in the task to dispense an item.
Along the way you will learn how to correctly wake up a task that is waiting for
input like this. You will also gain some experience implementing a Pend()
function yourself.
The provided hardware simulation will send you a keypad event via an
asynchronous call to the key_press_isr()
that should already be defined in
your pw_async2/codelab/main.cc file. It will pass you an integer value in
the range (0-9) to indicate which keypad button was pressed. It is going to be
up to you to process that keypad event safely, and allow your task to wait for
the keypad number after receiving a coin to dispense an item.
A single digit should be enough, but if you want an extra challenge, you can choose to allow larger numbers to be entered.
Solution for this step
1. Define a stub Keypad
class#
Lets start with a minimal stub implementation. Add the following declaration to your pw_async2/codelab/vending_machine.h header file:
class Keypad {
public:
constexpr Keypad() : key_pressed_(kNone) {}
// Pends until a key has been pressed, returning the key number.
//
// May only be called by one task.
pw::async2::Poll<int> Pend(pw::async2::Context& cx);
// Record a key press. Typically called from the keypad ISR.
void Press(int key);
private:
// A special internal value to indicate no keypad button has yet been
// pressed.
static constexpr int kNone = -1;
int key_pressed_;
};
Also add these stub implementations to the top of your pw_async2/codelab/vending_machine.cc file:
pw::async2::Poll<int> Keypad::Pend(pw::async2::Context& cx) {
// This is a stub implementation!
static_cast<void>(cx);
return key_pressed_;
}
void Keypad::Press(int key) {
// This is a stub implementation!
static_cast<void>(key);
}
This should be a good starting stub. Notice how the Pend
member function
just immediately returns the value of key_pressed_
, which is only ever set
to kNone
. We will fix that later, but let’s integrate the keypad into the
rest of the code first.
2. Add the Keypad
to the vending machine#
In your pw_async2/codelab/main.cc file, create a global instance of your
keypad type next to the coin slot instance, and then update your
VendingMachineTask
constructor to take a reference to it in the constructor,
and to save the reference as member data.
3. Wait for a key event in your task#
At this point, your task’s DoPend
function should look something like the
solution file for step 2 (though not necessarily identical):
1 if (!displayed_welcome_message_) {
2 PW_LOG_INFO("Welcome to the Pigweed Vending Machine!");
3 PW_LOG_INFO("Please insert a coin.");
4 displayed_welcome_message_ = true;
5 }
6
7 PW_TRY_READY_ASSIGN(unsigned coins, coin_slot_.Pend(cx));
8 PW_LOG_INFO(
9 "Received %u coin%s. Dispensing an item.", coins, coins > 1 ? "s" : "");
10
11 return pw::async2::Ready();
The logical place to handle the keypad input is after receiving a coin.
Update the coin received message to remove the “item is being dispensed” message. Instead we will wait for the keypad event.
Waiting for a keypad event is going to be very much like waiting for a coin.
Use the PW_TRY_READY_ASSIGN macro to poll keypad_.Pend(cx)
. If
it is ready, log the keypad key that was received, and that an item is
dispensing before returning pw::async2::Ready()
to finish the task.
4. Build and verify the stub#
Run your vending machine as before:
bazelisk run //pw_async2/codelab
You will see the welcome message, and you can insert a coin by again typing
c and pressing Enter. You should see a message that “-1” was
pressed. This is expected since the KeyPad::DoPend()
stub implementation
returns key_pressed_
, which was initialized to kNone
(-1).
INF Welcome to the Pigweed Vending Machine!
INF Please insert a coin.
c
INF Received 1 coin.
INF Please press a keypad key.
INF Keypad -1 was pressed. Dispensing an item.
So far so good! Next it is time to handle the hardware event, and have your task wait for the key press data.
5. Handle the event in your Keypad
implementation#
The first step should be trivial. Modify the stub key_press_isr
in your
pw_async2/codelab/main.cc to pass the key number to the Keypad::Press
member function.
void key_press_isr(int key) { keypad.Press(key); }
The next step is harder, implementing the Keypad::Press
member function
correctly.
Since the keypad ISR is asynchronous, you will need to synchronize access to the stored event data. For this codelab, we use InterruptSpinLock which is safe to acquire from an ISR in production use. Alternatively you can use atomic operations.
We’ll also use PW_GUARDED_BY
to add a compile-time check that the protected
members are accessed with the lock held.
Normally you would have to add the correct dependencies to the pw_async2/codelab/BUILD.bazel file, but we’ve already included them to save you some work. But if something went wrong, they are straightforward:
"//pw_sync:interrupt_spin_lock",
"//pw_sync:lock_annotations",
Add an instance of the spin lock to your
Keypad
class, along with a data member to hold the key press data.pw::sync::InterruptSpinLock lock_; int key_pressed_ PW_GUARDED_BY(lock_);
The
PW_GUARDED_BY(lock_)
just tells the compiler (clang) that to accesskey_pressed_
,lock_
should be held first, otherwise it should emit a diagnostic.Add two includes at the top of your pw_async2/codelab/vending_machine.h:
#include "pw_sync/interrupt_spin_lock.h" #include "pw_sync/lock_annotations.h"
Now you can implement
Keypad::Press
to save off the event data in a way that it can be safely read byKeypad::Pend
.std::lock_guard lock(lock_); key_pressed_ = key;
You can start off with this implementation for
Keypad::Pend
:std::lock_guard lock(lock_); int key = std::exchange(key_pressed_, kNone); if (key != kNone) { return key; } return pw::async2::Pending();
If you haven’t seen
std::exchange
used like this before, it just ensures that the key pressed event data is read only once by clearing it out tokNone
(-1) after reading the value ofkey_pressed_
.
It’s so simple… what could go wrong?
bazelisk run //pw_async2/codelab
INF Welcome to the Pigweed Vending Machine!
INF Please insert a coin.
c
INF Received 1 coin.
INF Please press a keypad key.
▄████▄ ██▀███ ▄▄▄ ██████ ██░ ██
▒██▀ ▀█ ▓██ ▒ ██▒ ▒████▄ ▒██ ▒ ▓██░ ██▒
▒▓█ 💥 ▄ ▓██ ░▄█ ▒ ▒██ ▀█▄ ░ ▓██▄ ▒██▀▀██░
▒▓▓▄ ▄██▒ ▒██▀▀█▄ ░██▄▄▄▄██ ▒ ██▒ ░▓█ ░██
▒ ▓███▀ ░ ░██▓ ▒██▒ ▓█ ▓██▒ ▒██████▒▒ ░▓█▒░██▓
░ ░▒ ▒ ░ ░ ▒▓ ░▒▓░ ▒▒ ▓▒█░ ▒ ▒▓▒ ▒ ░ ▒ ░░▒░▒
░ ▒ ░▒ ░ ▒░ ▒ ▒▒ ░ ░ ░▒ ░ ░ ▒ ░▒░ ░
░ ░░ ░ ░ ▒ ░ ░ ░ ░ ░░ ░
░ ░ ░ ░ ░ ░ ░ ░ ░
░
pw_async2/dispatcher_base.cc:151: PW_CHECK() or PW_DCHECK() FAILED!
FAILED ASSERTION
!task->wakers_.empty()
FILE & LINE
pw_async2/dispatcher_base.cc:151
FUNCTION
NativeDispatcherBase::RunOneTaskResult pw::async2::NativeDispatcherBase::RunOneTask(Dispatcher &, Task *)
MESSAGE
Task 0x7ffd8ddc2f40 returned Pending() without registering a waker
6. Fix the crash: Registering a waker#
We intentionally had you implement Keypad::Pend()
so it returned
Pending(), without storing a waker, as that
triggers an assertion. It is a clear signal that the code has no way of waking
up the task, so we crash on detecting it.
The crash message is there to help you, and be explicit about what went wrong, and may run into it yourself creating your own pendable types.
Generally if you are writing the leaf logic that decides that Pending() should be returned, then you should also store a Waker before returning that value.
Let’s fix Keypad::Pend()
so we store a waker using the context, which is
what is needed to eliminate the crash.
First include
pw_async2/waker.h
at the top of your pw_async2/codelab/vending_machine.h header.Add an instance as member data to your
Keypad
class.Note that the instance is internally thread-safe, and you do not need to guard it with a lock. An external spinlock is redundant, but harmless.
pw::async2::Waker waker_;
Setup the waker right before returning Pending
To do this correctly, let’s use PW_ASYNC_STORE_WAKER, giving it the context argument passed in to the
Pend()
, the waker to store to, and await_reason_string
to help debug issues.The change to the end of
Keypad::Pend
should look like this:+ PW_ASYNC_STORE_WAKER(cx, waker_, "keypad press"); return pw::async2::Pending();
Tip
Always pass a meaningful string for last
wait_reason_string
, as this will help you debug issues.
We haven’t yet modified Keypad::Press
to use the waker yet, and we will need
to. But first let’s show what happens if you forget this step. This time there
will not be a crash!
7. Forgetting to wake the task#
Let’s see what happens if you forget to wake the task.
Build and run the codelab, and then press c Enter 1 Enter.
bazelisk run //pw_async2/codelab
INF Welcome to the Pigweed Vending Machine!
INF Please insert a coin.
c
INF Received 1 coin.
INF Please press a keypad key.
1
As expected, nothing happens, not even an assertion. pw_async2
has no way of
knowing itself when the task is ready to be woken up as the pendable is ready.
You might wonder then how you would even debug this problem. Luckily, there is a way!
Try pressing d then Enter.
d
INF pw::async2::Dispatcher
INF Woken tasks:
INF Sleeping tasks:
INF - VendingMachineTask:0x7ffeec48fd90 (1 wakers)
INF * Waker 1: keypad press
This shows the state of all the tasks registered with the dispatcher.
Behind the scenes, the hardware.cc
implementation calls
LogRegisteredTasks on
the dispatcher which was registered via the HardwareInit()
function.
You can make this same call yourself to understand why your tasks aren’t doing anything, and investigate from there.
In this case we know we are sending a keypad press event, but obviously from
the Waker 1: keypad press
line in the output log, the task wasn’t properly
woken up.
To fix it, let’s add the missing Wake()
call to Keypad::Press
:
std::lock_guard lock(lock_);
key_pressed_ = key;
std::move(waker_).Wake();
Remember, the call to Wake()
consumes the waker, which is why we must move
the value out of waker_
first. In fact it won’t compile if you forget this
step because we want to make that consumption visible to the caller.
Tip
You can also end up in a “task not waking up” state if you destroy or
otherwise clear the Waker
instance that pointed at the task to wake.
Again LogRegisteredTasks
will point to a problem waking your task,
and give the last reason message, so you know where to start looking.
Important
If you don’t see the reason messages, you may have configured
PW_ASYNC2_DEBUG_WAIT_REASON to 0
to disable them.
LogRegisteredTasks
will still print out what it can, but for more
information you may need to consider enabling them temporarily.
8. Verify your event handler#
bazelisk run //pw_async2/codelab
Does it work as you expect?
Tip
If you suspect you didn’t implement your Keypad
class correctly,
comparing your solution against the pw_async2/codelab/coin_slot.cc
implementation might help before looking at the
pw_async2/codelab/solutions/step3 solution.
Well, depending on how you arranged to wait on both the CoinSlot
and
Keypad
in your DoPend
implementation, you could have one more problem.
We will look at how to better handle increasing complexity in your DoPend
function in the next step.
Step 4: Dealing with complexity#
You’ve now gotten to a point where your VendingMachineTask
has a
DoPend()
member function that:
First displays a welcome message, asking the user to insert a coin.
… unless it has been displayed already.
Then waits for the user to insert a coin.
… unless it has been inserted already.
Then waits for the user to select an item with the keypad.
We haven’t actually needed it yet, but we might also need to skip this if it has already occurred.
Writing DoPend()
functions this way is a perfectly valid choice, but you can
imagine the pattern of a chain of checks growing ever longer as the complexity
increases, and you end up with a long list of specialized conditional checks to
skip the early stages before you handle the later stages.
It’s also not ideal that we can’t process keypad input while waiting for a coin to be inserted. It would be nice to do something useful when the user makes a selection before paying for it. Likewise, when handling keypad input, we may miss additional coin insertion events when we should handle them, so we can properly account for the coins we are holding prior to a purchase.
This step shows you how to do this.
Solution for this step
1. Structuring your tasks as state machines#
The first thing we recommend is explicitly structuring your tasks as state
machines. For the vending machine you might end up with an enum for the states,
and a switch statement in DoPend
that looks like this skeleton:
enum State {
kWelcome,
kAwaitingPayment,
kAwaitingSelection,
};
pw::async2::Poll<> VendingMachineTask::DoPend(pw::async2::Context& cx) {
while (true) {
switch (state_) {
case kWelcome: {
// Show Welcome message
state_ = kAwaitingPayment;
break; // Reenter the switch()
}
case kAwaitingPayment: {
// Pend on coin_slot_
// Once coins are inserted...
state_ = kAwaitingSelection;
break; // Reenter the switch()
}
case kAwaitingSelection: {
// Pend on keypad_
// Once a selection is made
// Dispense item
return pw::async2::Ready();
}
}
}
}
This isn’t the only way to do it, but it is perhaps the easiest way to understand since there isn’t a lot of hidden machinery.
Go ahead and convert your implementation to use this pattern, and make sure it still works.
2. Waiting on multiple pendables#
Given that your VendingMachine
has both a CoinSlot
and a Keypad
,
there is already another problem in the simple linear flow we’ve implemented so
far.
You can see it for yourself by pressing 1 on the keypad first, and inserting a coin c afterwards, followed by Enter,
What happens in the linear flow, even after you’ve made the change to use a state machine pattern?
How do you make your task better at handling multiple inputs when
the Pend()
of CoinSlot
and Keypad
can only wait on one thing?
The answer is to use the Selector class and
the Select helper function to wait on multiple
pendables, along with the
VisitSelectResult helper that allows
you to unpack the completion result value when one of those pendables returns
Ready()
Using Selector
and Select
#
Select is a simple wrapper function for
Selector
. It constructs a temporary instance of the class, and then returns the result of callingPend()
on the instance. The temporary instance is then destroyed when the function returns the result.This behavior is useful when you have a set of pendables where you want to wait on any of them. However take note that this won’t ensure each pendable has a fair chance to report its stats. The first pendables in the set get polled first, and if those are ready, those take precedence.
Depending on the design of the pendable type, it may also not be possible to wait afresh for a new result after the pendable returned
Ready()
.Selector is a pendable class that polls an ordered set of pendables you provide to determine which (if any) are ready.
If you construct and store a
Selector
instance yourself, you can give all the pendables in the poll set a chance to returnReady()
, since each will be polled until the first time it returnsReady()
. However you must arrange to invoke thePend()
function on the sameSelector
instance yourself in a loop.Once you process the
AllPendablesCompleted
result when usingVisitSelectResult
(see below), you could then reset theSelector
to once again try all the pendables again.
For the vending machine, we’ll use Select
, so we can wait on multiple keypad
buttons and coins, and respond correctly.
Both Select
and Selector
work by using another helper function
PendableFor to construct a type-erased
wrapper that allows the Pend()
function to be called.
To poll both the CoinSlot
and the Keypad
, we use:
PW_TRY_READY_ASSIGN(
auto result,
pw::async2::Select(cx,
pw::async2::PendableFor<&CoinSlot::Pend>(coin_slot_),
pw::async2::PendableFor<&Keypad::Pend>(keypad_)));
We use auto
for the result return type, as the actual type is much more
complicated, and typing out the entire type would be laborious and would not
help with the readability of the code.
As usual, we’re using PW_TRY_READY_ASSIGN
so that if all the pendables are
pending then the current function will return Pending()
.
If one of those returns Ready()
, the result
value will indicate which
one, and will also be holding the value (if any). For the CoinSlot
that
value will be the count of coins added, and for the Keypad
, that will be the
button that was pressed.
Note
The result will only contain a single ready result, based on the order of the
pendables given to the Select
function (or used when constructing the
Selector
). They are checked in the order you give.
To get them all you may have to make the same call again. Keep in mind that
with Select
you could see more coin inserts if the ISR for them happens
to trigger faster than your task can poll for them.
The bare Selector
does not have that problem, but you will have to reset
its state instead to see more coin events after the first.
Using VisitSelectResult
#
VisitSelectResult is a helper for
processing the result of the Select
function or the Selector::Pend
member function call.
The result contains a single Ready()
result, but because of how the result
is stored, there is a bit of C++ template magic to unpack it for each possible
type. VisitSelectResult
does its best to hide most of the details, but you
need to specify an ordered list of lambda functions to handle each specific
pendable result.
pw::async2::VisitSelectResult(
result,
[](pw::async2::AllPendablesCompleted) {
// Special lambda that's only needed when using `Selector::Pend`, and
// which is invoked when all the other pendables have completed.
// This can be left blank when using `Select` as it is not used.
},
[&](unsigned coins) {
// This is the first lambda after the `AllPendablesCompleted`` case as
// `CoinSlot::Pend` was in the first argument to `Select`.
// Invoked if the `CoinSlot::Pend` is ready, with the count of coins
// returned as part of the `Poll` result from that call.
},
[&](int key) {
// This is the second lambda after the `AllPendablesCompleted`` case as
// `Keypad::Pend` was in the second argument to `Select`.
// Invoked if the `Keypad::Pend` is ready, with the key number
// returned as part of the `Poll` result from that call.
});
In case you were curious about the syntax, behind the scenes a std::visit
is
used with a std::variant
, and lambdas like these are how you can deal with
the alternative values.
But before you go and use Select
, there is one more suggestion.
3. Reusing the Select
code#
Both the kAwaitingPayment
state and the kAwaitingSelection
state are
going to be using the same set of pendables-the CoinSlot
and the Keypad
.
As the code involved is template-heavy (leading to lots of compile time code
being generated), it’s advisable to encapsulate the calls into a function that
both states can use, without expanding the templates twice.
The best way to do that is to treat input result as a pendable sub-state of the task’s primary state machine.
enum Input {
kNone,
kCoinInserted,
kKeyPressed,
};
pw::async2::Poll<Input> VendingMachineTask::PendInput(pw::async2::Context& cx) {
Input input = kNone;
PW_TRY_READY_ASSIGN(
auto result,
pw::async2::Select(cx,
pw::async2::PendableFor<&CoinSlot::Pend>(coin_slot_),
pw::async2::PendableFor<&Keypad::Pend>(keypad_)));
pw::async2::VisitSelectResult(
result,
[](pw::async2::AllPendablesCompleted) {},
[&](unsigned coins) {
/* do something with coins */
input = kCoinInserted;
},
[&](int key) {
/* do something with key */
input = kKeyPressed;
});
return input;
}
Inside the kAwaitingPayment
and kAwaitingSelection
states, you can then
Pend()
for and then switch on the input sub-state result:
switch (state_) {
// …
case kAwaitingPayment: {
PW_TRY_READY_ASSIGN(Input input, PendInput(cx));
switch (input) {
case kCoinInserted: {
/* react to the expected coins */
break;
}
case kKeyPressed: {
/* react to the unexpected input */
break;
}
}
}
// And then something similar for the kAwaitingSelection state.
}
Now go ahead and try filling in the blanks in those snippets. Can you build something reasonable that handles out-of-order input?
Remember, if you get stuck, you can reference our example solution for this step: pw_async2/codelab/solutions/step4
Step 5: Communicating between tasks#
Now that the VendingMachineTask
has been refactored, it’s ready to handle
more functionality. In this step, you’ll write code to handle the vending
machine’s dispenser mechanism. Along the way, you’ll learn how to send data
between tasks.
This vending machine uses a motor to push the selected product into a chute. A sensor detects when the item has dropped, then the motor is turned off.
The dispenser mechanism is complex enough to merit a task of its own. The
VendingMachineTask
will send which items to dispense to a new
DispenserTask
. After dispensing an item, the DispenserTask
will send
confirmation back to the VendingMachineTask
.
Solution for this step
1. Set up the item_drop_sensor_isr()
#
First, let’s set up the item drop sensor. When an item is dispensed
successfully, the item drop sensor triggers an interrupt, which is handled by
the item_drop_sensor_isr()
function.
void item_drop_sensor_isr();
We’ve provided an ItemDropSensor
class in
pw_async2/codelab/item_drop_sensor.h. It is similar to the CoinSlot
and
Keypad
classes.
To use it, #include "item_drop_sensor.h"
and declare an ItemDropSensor
instance in your pw_async2/codelab/main.cc:
codelab::ItemDropSensor item_drop_sensor;
Then, call it from item_drop_sensor_isr()
.
void item_drop_sensor_isr() { item_drop_sensor.Drop(); }
2. Setting up inter-task communication#
We’ll be adding a new DispenserTask
soon. To get ready for that, let’s set
up communications channels between VendingMachineTask
and the new task.
There are many ways to use a Waker to communicate between tasks. For this step, we’ll use pw::InlineAsyncQueue to send events between the two tasks.
We’ll need two queues, one for each of the following:
Send dispense requests (item numbers) from the
VendingMachineTask
to theDispenserTask
.Send dispense responses (success/failure) from the
DispenserTask
to theVendingMachineTask
.
For convenience, you can create aliases for these queues in
pw_async2/codelab/vending_machine.h. A depth of 1
is fine for now.
using DispenseRequestQueue = pw::InlineAsyncQueue<int, 1>;
using DispenseResponseQueue = pw::InlineAsyncQueue<bool, 1>;
Make sure to add #include` "pw_containers/inline_async_queue.h"
to the top
of the file.
Declare a dispense_requests
queue and a dispense_response
queue in your
pw_async2/codelab/main.cc.
3. Create a new DispenserTask
#
The DispenserTask
will turn the dispenser motor on and off in response to
dispense requests from the VendingMachineTask
.
Like VendingMachineTask
, DispenserTask
will be a state machine. It will
need to handle three states:
kIdle
– waiting for a dispense request; motor is offkDispensing
– actively dispensing an item; motor is onkReportDispenseSuccess
– waiting to report success; motor is offkReportDispenseFailure
– waiting to report failure; motor is off
The task will control the vending machine’s dispenser motor with the
SetDispenserMotorState
function in pw_async2/codelab/hardware.h.
enum MotorState { kOff, kOn };
// Call this to set the simulated dispenser motor state for a item slot (1-4).
// The motor state for each item slot is initially off.
void SetDispenserMotorState(int item, MotorState state);
Declare a DispenserTask
in your pw_async2/codelab/vending_machine.h
file. It should take references to ItemDropSensor
and the two queues in its
constructor.
class DispenserTask : public pw::async2::Task {
public:
DispenserTask(ItemDropSensor& item_drop_sensor,
DispenseRequestQueue& dispense_requests,
DispenseResponseQueue& dispense_responses)
: pw::async2::Task(PW_ASYNC_TASK_NAME("DispenserTask")),
item_drop_sensor_(item_drop_sensor),
dispense_requests_(dispense_requests),
dispense_responses_(dispense_responses),
state_{kIdle} {}
private:
static constexpr auto kDispenseTimeout = std::chrono::seconds(5);
enum State {
kIdle,
kDispensing,
kReportDispenseSuccess,
kReportDispenseFailure,
};
pw::async2::Poll<> DoPend(pw::async2::Context& cx) override;
ItemDropSensor& item_drop_sensor_;
DispenseRequestQueue& dispense_requests_;
DispenseResponseQueue& dispense_responses_;
State state_;
};
The implementation should be structured as a state machine. You can copy this stub to your pw_async2/codelab/vending_machine.cc:
pw::async2::Poll<> DispenserTask::DoPend(pw::async2::Context& cx) {
// This is a stub implementation!
while (true) {
switch (state_) {
case kIdle: {
break;
}
case kDispensing: {
break;
}
case kReportDispenseSuccess: {
break;
}
case kReportDispenseFailure: {
break;
}
}
}
}
Here’s what the three states need to do.
kIdle
Read an item number from the
dispense_requests_
queue. This is done by calling PendNotEmpty() and accessing the request with a call tofront()
. Keep the item in the queue until you turn off the dispensing motor; you’ll need to reference the number.Call
item_drop_sensor_.Clear()
so the sensor is ready for a new item.Start the motor with a call to
SetDispenserMotorState()
.Move to the
kDispensing
state.
kDispensing
Wait for the
ItemDropSensor
to trigger withitem_drop_sensor_.Pend()
.Turn off the dispensing motor, using
dispense_requests_.front()
for the motor number.pop()
the dispense request. It’s no longer needed since the motor is off.Advance to the
kReportDispenseSuccess
state.
kReportDispenseSuccess
Wait for the response queue to have space with PendHasSpace().
Signal that the item was dispensed with
.push(true)
.Note that dispensing can’t fail at this stage—we’ll get to that later.
4. Interact with DispenserTask
from VendingMachineTask
#
Now, let’s get VendingMachineTask
communicating with DispenserTask
.
Instead of just logging when a purchase is made, VendingMachineTask
will
send the selected item to the DispenserTask
through the dispense requests
queue. Then it will wait for a response with the dispense responses queue.
Update VendingMachineTask
’s constructor to take references to the two
queues.
We’ll need two new states in VendingMachineTask
for this:
kAwaitingDispenseIdle
state.This state ensures the
DispenserTask
is ready for the request before we send it.Transition to this state after
kAwaitingSelection
.Wait for a slot in the dispense requests queue by calling PendHasSpace().
Push the request to it.
Transition to the new
kAwaitingDispense
state.
kAwaitingDispense
stateWait for
DispenserTask
to report that it finished dispensing the item with a call to PendNotEmpty().Display a message with the result.
Return to the
kWelcome
state if successful orkAwaitingSelection
if not.
5. Build and run: Test the dispenser#
Build and run the codelab, and then press c Enter 1 Enter to input a coin and make a selection.
bazelisk run //pw_async2/codelab
INF Welcome to the Pigweed Vending Machine!
INF Please insert a coin.
c
INF Received 1 coin.
INF Please press a keypad key.
1
You’ll notice that the vending machine hasn’t finished dispensing the item.
Press i Enter to signal that the item has dropped, triggering the
item_drop_sensor_isr()
. You should see vending machine display its welcome
message again.
Congratulations! You now have a fully functioning vending machine!
6. Handling unexpected situations with timeouts#
But wait—what if you press the wrong button and accidentally buy an out-of-stock item? Well, as of now, the dispenser will just keep running forever. The vending machine will eat your money while you go hungry.
Let’s fix this. We can add a timeout to the kDispensing
state. If the
ItemDropSensor
hasn’t triggered after a certain amount of time, then
something has gone wrong. The DispenserTask
should stop the motor and tell
the VendingMachineTask
what happened.
You can implement a timeout with pw::async2::TimeFuture. To use it,
#include "pw_async2/time_provider.h"
and declare a TimeFuture
in your
DispenserTask
.
pw::async2::TimeFuture<pw::chrono::SystemClock> timeout_future_;
Define a timeout period in your DispenserTask
. For testing purposes, make
sure it’s long enough for a human to respond. 5 seconds should do.
static constexpr auto kDispenseTimeout = std::chrono::seconds(5);
When you start dispensing an item (in your transition from kIdle
to
kDispensing
), initialize the TimeFuture
to your timeout.
const auto expected_completion =
pw::chrono::SystemClock::TimePointAfterAtLeast(kDispenseTimeout);
timeout_future_ =
pw::async2::GetSystemTimeProvider().WaitUntil(expected_completion);
Then, in the kDispensing
state, use Select
to wait for either the timeout or the item drop signal, whichever comes first.
Use VisitSelectResult to take action
based on the result:
pw::async2::VisitSelectResult(
result,
[](pw::async2::AllPendablesCompleted) {},
[&](pw::async2::ReadyType) {
// Received the item drop interrupt.
// Note that the type is ReadyType, the type of Ready(). Ready() is an
// empty placeholder produced by a completed Poll<>.
},
[&](std::chrono::time_point<pw::chrono::SystemClock>) {
// The timeout occurred before the item drop interrupt!
});
If the item drop interrupt arrives first, clear the timeout with
timeout_future_ = {}
. If the timer isn’t cleared, it will fire later and wakeDispenserTask
unnecessarily, wasting time and power. After that, proceed to thekReportDispenseSuccess
state.If the timeout arrives first, proceed to the
kReportDispenseSuccess
state.
In either case, be sure to turn off the motor and pop()
the dispense request
from the queue.
7. Build and run: Test the dispenser with timeouts#
Build and run the codelab, and then press c Enter 1 Enter to input a coin and make a selection.
bazelisk run //pw_async2/codelab
The machine will start dispensing, but don’t press i. After the timeout period, you should see the dispenser time out and ask you to make another selection.
INF Welcome to the Pigweed Vending Machine!
INF Please insert a coin.
INF Dispenser task awake
c
INF Received 1 coin.
INF Please press a keypad key.
1
INF Keypad 1 was pressed. Dispensing an item.
INF [Motor for item 1 set to On]
INF [Motor for item 1 set to Off]
INF Dispense failed. Choose another selection.
Try again, but this time press i Enter quickly so dispensing the item succeeds.