4. Use a state machine#
pw_async2: Cooperative async tasks for embedded
The DoPend()
implementation in your VendingMachineTask
now:
Displays a welcome message
Prompts the user to insert a coin… unless they have been prompted already
Waits for the user to insert a coin… unless coins have been inserted already
Waits for the user to select an item with the keypad
Implementing DoPend()
like this is valid, but you can imagine the chain of
checks growing ever longer as the complexity increases. You’d 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 you can’t process keypad input
while waiting for coins to be inserted.
To manage this complexity, we recommend explicitly structuring your tasks as state machines.
Refactor your task as a state machine#
Set up the state machine data in
vending_machine.h
:Declare a private
State
enum inVendingMachineTask
that represents all possible states:kWelcome
kAwaitingPayment
kAwaitingSelection
Declare a private
State state_
data member that stores the current stateInitialize
state_
in theVendingMachineTask
constructorRemove the
displayed_welcome_message_
data member which is no longer needed (a good sign!) and remove its initialization in the constructor
Hint
1class VendingMachineTask : public pw::async2::Task { 2 public: 3 VendingMachineTask(CoinSlot& coin_slot, Keypad& keypad) 4 : pw::async2::Task(PW_ASYNC_TASK_NAME("VendingMachineTask")), 5 coin_slot_(coin_slot), 6 keypad_(keypad), 7 // Delete the displayed_welcome_message_ initializer here 8 coins_inserted_(0), 9 state_(kWelcome) {} 10 11 private: 12 // This is the core of the asynchronous task. The dispatcher calls this method 13 // to give the task a chance to do work. 14 pw::async2::Poll<> DoPend(pw::async2::Context& cx) override; 15 16 CoinSlot& coin_slot_; 17 // Delete the displayed_welcome_message_ declaration here 18 Keypad& keypad_; 19 unsigned coins_inserted_; 20 21 enum State { 22 kWelcome, 23 kAwaitingPayment, 24 kAwaitingSelection, 25 }; 26 State state_; 27};
Refactor the
DoPend()
implementation invending_machine.cc
as an explicit state machine:Handle the
kWelcome
,kAwaitingPayment
, andkAwaitingSelection
states (as well as the transitions between states)
Hint
1pw::async2::Poll<> VendingMachineTask::DoPend(pw::async2::Context& cx) { 2 while (true) { 3 switch (state_) { 4 case kWelcome: { 5 PW_LOG_INFO("Welcome to the Pigweed Vending Machine!"); 6 PW_LOG_INFO("Please insert a coin."); 7 state_ = kAwaitingPayment; 8 break; 9 } 10 case kAwaitingPayment: { 11 PW_TRY_READY_ASSIGN(unsigned coins, coin_slot_.Pend(cx)); 12 PW_LOG_INFO("Received %u coin%s.", coins, coins > 1 ? "s" : ""); 13 PW_LOG_INFO("Please press a keypad key."); 14 coins_inserted_ += coins; 15 state_ = kAwaitingSelection; 16 break; 17 } 18 case kAwaitingSelection: { 19 PW_TRY_READY_ASSIGN(int key, keypad_.Pend(cx)); 20 PW_LOG_INFO("Keypad %d was pressed. Dispensing an item.", key); 21 return pw::async2::Ready(); 22 } 23 } 24 } 25}
This isn’t the only way to do it, but it is perhaps the easiest to understand since there isn’t a lot of hidden machinery.
Problem with linear flow#
Even with the task refactored as an explicit state machine, there’s still a problem with the simple linear flow we’ve implemented so far. Watch what happens when you make a selection before inserting coins:
Press 1 c Enter.
You should see output like this:
INF Welcome to the Pigweed Vending Machine! INF Please insert a coin. 1c INF Received 1 coin. INF Please press a keypad key. INF Keypad 1 was pressed. Dispensing an item.
When the vending machine received a coin, it immediately dispensed an item based on keypad input that occurred before the coin insertion. Imagine inserting money into a real vending machine, and then it automatically dispenses some random item, because someone else had previously come along an hour ago and touched the button for that item!
In other words, how do you make your task better at handling multiple inputs
when CoinSlot
and Keypad
can each 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()
.
Wait on a set of pendables#
When multiple states use the same set of pendables (e.g. kAwaitingPayment
and kAwaitingSelection
both use CoinSlot
and Keypad
) it’s best to
encapsulate the calls into a function that both states can use. The code for
waiting on a set of pendables is template-heavy, which can lead to lots of
compiler-generated code. Encapsulating the calls into a function reduces the
number of times that templates need to be expanded.
For the vending machine, we can unify coin insertions and keypad selections
using an Input
enum, and a function PendInput
for pending on either.
Set up the
Input
enum invending_machine.h
:Add an
Input
enum with the following states:kNone
kCoinInserted
kKeyPressed
Declare a new
PendInput()
method:1 // Waits for either an inserted coin or keypress to occur, updating either 2 // `coins_inserted_` or `selected_item_` accordingly. 3 pw::async2::Poll<Input> PendInput(pw::async2::Context& cx);
Include the
<optional>
headerAdd a new
std::optional<int> selected_item_
data member
Hint
1class VendingMachineTask : public pw::async2::Task { 2 public: 3 VendingMachineTask(CoinSlot& coin_slot, Keypad& keypad) 4 : pw::async2::Task(PW_ASYNC_TASK_NAME("VendingMachineTask")), 5 coin_slot_(coin_slot), 6 keypad_(keypad), 7 coins_inserted_(0), 8 state_(kWelcome) {} 9 10 private: 11 enum Input { 12 kNone, 13 kCoinInserted, 14 kKeyPressed, 15 }; 16 17 // This is the core of the asynchronous task. The dispatcher calls this method 18 // to give the task a chance to do work. 19 pw::async2::Poll<> DoPend(pw::async2::Context& cx) override; 20 21 // Waits for either an inserted coin or keypress to occur, updating either 22 // `coins_inserted_` or `selected_item_` accordingly. 23 pw::async2::Poll<Input> PendInput(pw::async2::Context& cx); 24 25 CoinSlot& coin_slot_; 26 Keypad& keypad_; 27 unsigned coins_inserted_; 28 29 enum State { 30 kWelcome, 31 kAwaitingPayment, 32 kAwaitingSelection, 33 }; 34 State state_; 35 36 std::optional<int> selected_item_; 37};
Update the
VendingMachineTask
implementation invending_machine.cc
:Implement
PendInput()
:1pw::async2::Poll<VendingMachineTask::Input> VendingMachineTask::PendInput( 2 pw::async2::Context& cx) { 3 Input input = kNone; 4 selected_item_ = std::nullopt; 5 6 PW_TRY_READY_ASSIGN( 7 auto result, 8 pw::async2::Select(cx, 9 pw::async2::PendableFor<&CoinSlot::Pend>(coin_slot_), 10 pw::async2::PendableFor<&Keypad::Pend>(keypad_))); 11 pw::async2::VisitSelectResult( 12 result, 13 [](pw::async2::AllPendablesCompleted) {}, 14 [&](unsigned coins) { 15 coins_inserted_ += coins; 16 input = kCoinInserted; 17 }, 18 [&](int key) { 19 selected_item_ = key; 20 input = kKeyPressed; 21 }); 22 23 return input; 24}
Refactor
DoPend()
to useInput
:1pw::async2::Poll<> VendingMachineTask::DoPend(pw::async2::Context& cx) { 2 while (true) { 3 switch (state_) { 4 case kWelcome: { 5 PW_LOG_INFO("Welcome to the Pigweed Vending Machine!"); 6 PW_LOG_INFO("Please insert a coin."); 7 state_ = kAwaitingPayment; 8 break; 9 } 10 11 case kAwaitingPayment: { 12 PW_TRY_READY_ASSIGN(Input input, PendInput(cx)); 13 switch (input) { 14 case kCoinInserted: 15 PW_LOG_INFO("Received %u coin%s.", 16 coins_inserted_, 17 coins_inserted_ != 1 ? "s" : ""); 18 if (coins_inserted_ > 0) { 19 PW_LOG_INFO("Please press a keypad key."); 20 state_ = kAwaitingSelection; 21 } 22 break; 23 case kKeyPressed: 24 PW_LOG_ERROR("Please insert a coin before making a selection."); 25 break; 26 case kNone: 27 break; 28 } 29 break; 30 } 31 32 case kAwaitingSelection: { 33 PW_TRY_READY_ASSIGN(Input input, PendInput(cx)); 34 switch (input) { 35 case kCoinInserted: 36 PW_LOG_INFO("Received a coin. Your balance is currently %u coins.", 37 coins_inserted_); 38 PW_LOG_INFO("Press a keypad key to select an item."); 39 break; 40 case kKeyPressed: 41 if (!selected_item_.has_value()) { 42 state_ = kAwaitingSelection; 43 continue; 44 } 45 PW_LOG_INFO("Keypad %d was pressed. Dispensing an item.", 46 selected_item_.value()); 47 // Pay for the item. 48 coins_inserted_ = 0; 49 return pw::async2::Ready(); 50 case kNone: 51 break; 52 } 53 break; 54 } 55 } 56 } 57 58 PW_UNREACHABLE; 59}
Get yourself a $FAVORITE_BEVERAGE
. There’s a lot to explain.
Let’s start with the PendInput()
implementation. The
PW_TRY_READY_ASSIGN invocation uses Select()
to wait on the coin insertion and keypad press pendables:
1 PW_TRY_READY_ASSIGN(
2 auto result,
3 pw::async2::Select(cx,
4 pw::async2::PendableFor<&CoinSlot::Pend>(coin_slot_),
5 pw::async2::PendableFor<&Keypad::Pend>(keypad_)));
As usual, we’re using PW_TRY_READY_ASSIGN
so that if all the pendables are
pending then the current function will return Pending()
.
We use auto
for the result return type because the actual type is very
complicated. Typing out the entire type would be laborious and would reduce
code readability.
Select()
checks the pendables in the order provided. result
will only
contain a single result, from whatever pendable was ready. To get another
result you’d have to call Select()
again.
Note that each pendable doesn’t have a fair chance to do work. The first pendable gets polled first, and if it’s ready, it takes precedence.
Note
Selector is another way to wait on a set of pendables. It’s 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 return Ready()
, since each will
be polled until the first time it returns Ready()
. However you must
arrange to invoke the Pend()
function on the same Selector
instance
yourself in a loop.
Once you process the AllPendablesCompleted
result when using
VisitSelectResult()
(explained below), you could then reset the
Selector
to once again try all the pendables again.
The PendableFor helper function used in the
Select()
invocation constructs a type-erased wrapper that makes it easy to
call the Pend()
method from any pendable.
Moving on, VisitSelectResult is a helper
for processing the result of the Select
function or the Selector::Pend()
member function call:
1 pw::async2::VisitSelectResult(
2 result,
3 [](pw::async2::AllPendablesCompleted) {},
4 [&](unsigned coins) {
5 coins_inserted_ += coins;
6 input = kCoinInserted;
7 },
8 [&](int key) {
9 selected_item_ = key;
10 input = kKeyPressed;
11 });
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. The order must match the ordering that was used in the
Select()
call:
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.
});
Note
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.
As for the updates to DoPend()
, it’s mostly the same logic as before, with
the addition of some more switch
logic to handle the new Input
encapsulation.
Next steps#
Continue to 5. Communicate between tasks to learn how to spin up a new task and get your tasks communicating with each other.
Checkpoint#
At this point, your code should look similar to the files below.
#include "coin_slot.h"
#include "hardware.h"
#include "pw_async2/dispatcher.h"
#include "vending_machine.h"
namespace {
codelab::CoinSlot coin_slot;
codelab::Keypad keypad;
} // namespace
// Interrupt handler function invoked when the user inserts a coin into the
// vending machine.
void coin_inserted_isr() { coin_slot.Deposit(); }
// Interrupt handler function invoked when the user presses a key on the
// machine's keypad. Receives the value of the pressed key (0-9).
void key_press_isr(int key) { keypad.Press(key); }
// Interrupt handler function invoked to simulate the item drop detector
// detecting confirmation that an item was successfully dispensed from the
// machine.
void item_drop_sensor_isr() {
// In Step 5 you will uses this as part of a new Dispense task that runs
// the dispenser motor until an item drops, or you time out on the vend
// operation.
}
int main() {
pw::async2::Dispatcher dispatcher;
codelab::HardwareInit(&dispatcher);
codelab::VendingMachineTask task(coin_slot, keypad);
dispatcher.Post(task);
dispatcher.RunToCompletion();
return 0;
}
#include "vending_machine.h"
#include "pw_async2/pendable.h"
#include "pw_async2/select.h"
#include "pw_async2/try.h"
#include "pw_log/log.h"
namespace codelab {
pw::async2::Poll<int> Keypad::Pend(pw::async2::Context& cx) {
std::lock_guard lock(lock_);
int key = std::exchange(key_pressed_, kNone);
if (key != kNone) {
return key;
}
PW_ASYNC_STORE_WAKER(cx, waker_, "keypad press");
return pw::async2::Pending();
}
void Keypad::Press(int key) {
std::lock_guard lock(lock_);
key_pressed_ = key;
std::move(waker_).Wake();
}
pw::async2::Poll<VendingMachineTask::Input> VendingMachineTask::PendInput(
pw::async2::Context& cx) {
Input input = kNone;
selected_item_ = std::nullopt;
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) {
coins_inserted_ += coins;
input = kCoinInserted;
},
[&](int key) {
selected_item_ = key;
input = kKeyPressed;
});
return input;
}
pw::async2::Poll<> VendingMachineTask::DoPend(pw::async2::Context& cx) {
while (true) {
switch (state_) {
case kWelcome: {
PW_LOG_INFO("Welcome to the Pigweed Vending Machine!");
PW_LOG_INFO("Please insert a coin.");
state_ = kAwaitingPayment;
break;
}
case kAwaitingPayment: {
PW_TRY_READY_ASSIGN(Input input, PendInput(cx));
switch (input) {
case kCoinInserted:
PW_LOG_INFO("Received %u coin%s.",
coins_inserted_,
coins_inserted_ != 1 ? "s" : "");
if (coins_inserted_ > 0) {
PW_LOG_INFO("Please press a keypad key.");
state_ = kAwaitingSelection;
}
break;
case kKeyPressed:
PW_LOG_ERROR("Please insert a coin before making a selection.");
break;
case kNone:
break;
}
break;
}
case kAwaitingSelection: {
PW_TRY_READY_ASSIGN(Input input, PendInput(cx));
switch (input) {
case kCoinInserted:
PW_LOG_INFO("Received a coin. Your balance is currently %u coins.",
coins_inserted_);
PW_LOG_INFO("Press a keypad key to select an item.");
break;
case kKeyPressed:
if (!selected_item_.has_value()) {
state_ = kAwaitingSelection;
continue;
}
PW_LOG_INFO("Keypad %d was pressed. Dispensing an item.",
selected_item_.value());
// Pay for the item.
coins_inserted_ = 0;
return pw::async2::Ready();
case kNone:
break;
}
break;
}
}
}
PW_UNREACHABLE;
}
} // namespace codelab
#pragma once
#include <optional>
#include "coin_slot.h"
#include "pw_async2/context.h"
#include "pw_async2/poll.h"
#include "pw_async2/task.h"
#include "pw_async2/waker.h"
#include "pw_sync/interrupt_spin_lock.h"
#include "pw_sync/lock_annotations.h"
namespace codelab {
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;
pw::sync::InterruptSpinLock lock_;
int key_pressed_ PW_GUARDED_BY(lock_);
pw::async2::Waker waker_; // No guard needed!
};
// The main task that drives the vending machine.
class VendingMachineTask : public pw::async2::Task {
public:
VendingMachineTask(CoinSlot& coin_slot, Keypad& keypad)
: pw::async2::Task(PW_ASYNC_TASK_NAME("VendingMachineTask")),
coin_slot_(coin_slot),
keypad_(keypad),
coins_inserted_(0),
state_(kWelcome) {}
private:
enum Input {
kNone,
kCoinInserted,
kKeyPressed,
};
// This is the core of the asynchronous task. The dispatcher calls this method
// to give the task a chance to do work.
pw::async2::Poll<> DoPend(pw::async2::Context& cx) override;
// Waits for either an inserted coin or keypress to occur, updating either
// `coins_inserted_` or `selected_item_` accordingly.
pw::async2::Poll<Input> PendInput(pw::async2::Context& cx);
CoinSlot& coin_slot_;
Keypad& keypad_;
unsigned coins_inserted_;
enum State {
kWelcome,
kAwaitingPayment,
kAwaitingSelection,
};
State state_;
std::optional<int> selected_item_;
};
} // namespace codelab