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
Stateenum inVendingMachineTaskthat represents all possible states:kWelcome(to explicitly handle the welcome message)kAwaitingPaymentkAwaitingSelection
Declare a private
State state_data member that stores the current stateInitialize
state_in theVendingMachineTaskconstructor
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 // This is the core of the asynchronous task. The dispatcher calls this method 12 // to give the task a chance to do work. 13 pw::async2::Poll<> DoPend(pw::async2::Context& cx) override; 14 15 CoinSlot& coin_slot_; 16 CoinFuture coin_future_; 17 Keypad& keypad_; 18 KeyPressFuture key_future_; 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.ccas an explicit state machine:Handle the
kWelcome,kAwaitingPayment, andkAwaitingSelectionstates (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 11 case kAwaitingPayment: { 12 if (!coin_future_.is_pendable()) { 13 coin_future_ = coin_slot_.GetCoins(); 14 } 15 PW_TRY_READY_ASSIGN(unsigned coins, coin_future_.Pend(cx)); 16 PW_LOG_INFO("Received %u coin%s.", coins, coins > 1 ? "s" : ""); 17 PW_LOG_INFO("Please press a keypad key."); 18 coins_inserted_ += coins; 19 state_ = kAwaitingSelection; 20 break; 21 } 22 23 case kAwaitingSelection: { 24 if (!key_future_.is_pendable()) { 25 key_future_ = keypad_.WaitForKeyPress(); 26 } 27 PW_TRY_READY_ASSIGN(int key, key_future_.Pend(cx)); 28 PW_LOG_INFO("Keypad %d was pressed. Dispensing an item.", key); 29 return pw::async2::Ready(); 30 } 31 } 32 } 33}
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. For example, 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. 1 c INF Received 1 coin. INF Please press a keypad key.
After pressing 1, nothing happens. You get no feedback, and it’s not clear whether the machine is still waiting or stuck. Ideally, the machine would detect the keypress and tell the user that it still needs coins.
In other words, how do you make your task better at handling multiple inputs
when CoinFuture and KeyPressFuture can each only wait on one thing?
The answer is to use the Select helper function
to wait on multiple futures, returing a result whenever the first future
is Ready().
Wait on a set of futures#
When multiple states use the same set of futures (e.g. kAwaitingPayment
and kAwaitingSelection both use CoinSlot and Keypad) it’s efficient
to combine them into a single future that pends on both.
For the vending machine, we can unify coin insertions and keypad selections using SelectFuture, which completes when the first of the input futures completes, returning an OptionalTuple of results.
Include
"pw_async2/select.h"invending_machine.h.Add a
select_future_data member toVendingMachineTaskto hold both aCoinFutureand aKeyPressFuture:1 CoinSlot& coin_slot_; 2 Keypad& keypad_; 3 pw::async2::SelectFuture<CoinFuture, KeyPressFuture> select_future_;
Just as with the separate futures, we store the
SelectFutureas a member to hold the state of the operations acrossPend()calls.Note
SelectFutureautomatically resets all of its child futures when it returnsReady(), so we must assign a new one (using Select) when we want to wait again.Update the
VendingMachineTaskimplementation invending_machine.cc:Refactor
DoPend()to useselect_future_: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 if (!select_future_.is_pendable()) { 13 select_future_ = pw::async2::Select(coin_slot_.GetCoins(), 14 keypad_.WaitForKeyPress()); 15 } 16 PW_TRY_READY_ASSIGN(auto result, select_future_.Pend(cx)); 17 if (result.has_value<0>()) { 18 unsigned coins = result.value<0>(); 19 PW_LOG_INFO("Received %u coin%s.", coins, coins > 1 ? "s" : ""); 20 PW_LOG_INFO("Please press a keypad key."); 21 coins_inserted_ += coins; 22 state_ = kAwaitingSelection; 23 } else { 24 PW_LOG_ERROR("Please insert a coin before making a selection."); 25 } 26 break; 27 } 28 29 case kAwaitingSelection: { 30 if (!select_future_.is_pendable()) { 31 select_future_ = pw::async2::Select(coin_slot_.GetCoins(), 32 keypad_.WaitForKeyPress()); 33 } 34 PW_TRY_READY_ASSIGN(auto result, select_future_.Pend(cx)); 35 if (result.has_value<1>()) { 36 int key = result.value<1>(); 37 PW_LOG_INFO("Keypad %d was pressed. Dispensing an item.", key); 38 return pw::async2::Ready(); 39 } 40 41 // If we didn't get a key press, we must have gotten a coin. 42 coins_inserted_ += result.value<0>(); 43 PW_LOG_INFO("Received a coin. Your balance is currently %u coins.", 44 coins_inserted_); 45 PW_LOG_INFO("Please press a keypad key."); 46 break; 47 } 48 } 49 } 50}
Grab yourself a $FAVORITE_BEVERAGE and let’s dig into our new DoPend()
implementation.
In the kAwaitingPayment state, we first check if select_future_ can be
pended. If not, we initialize it using Select,
providing a new CoinFuture and KeyPressFuture.
1 if (!select_future_.is_pendable()) {
2 select_future_ = pw::async2::Select(coin_slot_.GetCoins(),
3 keypad_.WaitForKeyPress());
4 }
Then, we pend the select_future_:
1 PW_TRY_READY_ASSIGN(auto result, select_future_.Pend(cx));
Previously, we used PW_TRY_READY_ASSIGN to wait for a single future. Here,
we use it to wait for both the CoinFuture and KeyPressFuture at once.
When the first of these futures completes, SelectFuture also completes and
returns its result.
The type of this result is a pw::OptionalTuple<unsigned, int>, where
unsigned and int are the return types of CoinFuture and
KeyPressFuture, respectively. This utility behaves similarly to a
std::tuple<std::optional<unsigned>, std::optional<int>>, but is optimized
for space by using a bitfield to track presence.
We then check which future completed using has_value(). There are two
overloads of this method:
has_value<size_t>(): Returns true if the future at the given index (in the order they were passed toSelect()) completed.has_value<T>(): Returns true if the future with return typeTcompleted. This can only be used if each future has a distinct return type.
Once we know which future completed, we can retrieve its result using
result.value<T>() (or result.value<size_t>()) and act on it:
1 if (result.has_value<0>()) {
2 unsigned coins = result.value<0>();
3 PW_LOG_INFO("Received %u coin%s.", coins, coins > 1 ? "s" : "");
4 PW_LOG_INFO("Please press a keypad key.");
5 coins_inserted_ += coins;
6 state_ = kAwaitingSelection;
7 } else {
8 PW_LOG_ERROR("Please insert a coin before making a selection.");
9 }
The logic for kAwaitingSelection is similar, but we first check for
the result of KeyPressFuture:
1 if (result.has_value<1>()) {
2 int key = result.value<1>();
3 PW_LOG_INFO("Keypad %d was pressed. Dispensing an item.", key);
4 return pw::async2::Ready();
5 }
6
7 // If we didn't get a key press, we must have gotten a coin.
8 coins_inserted_ += result.value<0>();
9 PW_LOG_INFO("Received a coin. Your balance is currently %u coins.",
10 coins_inserted_);
11 PW_LOG_INFO("Please press a keypad key.");
12 break;
By using Select, we can now handle either coin insertions or keypad
selections no matter which state the vending machine is in.
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/basic_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::BasicDispatcher dispatcher;
codelab::HardwareInit(&dispatcher);
codelab::VendingMachineTask task(coin_slot, keypad);
dispatcher.Post(task);
dispatcher.RunToCompletion();
return 0;
}
#include "vending_machine.h"
#include <mutex>
#include "pw_async2/try.h"
#include "pw_log/log.h"
namespace codelab {
pw::async2::Poll<int> KeyPressFuture::Pend(pw::async2::Context& cx) {
return core_.DoPend(*this, cx);
}
KeyPressFuture::~KeyPressFuture() {
std::lock_guard lock(internal::KeypadLock());
core_.Unlist();
}
pw::async2::Poll<int> KeyPressFuture::DoPend(pw::async2::Context&) {
std::lock_guard lock(internal::KeypadLock());
if (key_pressed_.has_value()) {
return pw::async2::Ready(*key_pressed_);
}
return pw::async2::Pending();
}
KeyPressFuture Keypad::WaitForKeyPress() {
std::lock_guard lock(internal::KeypadLock());
KeyPressFuture future(pw::async2::FutureState::kPending);
futures_.Push(future.core_);
return future;
}
void Keypad::Press(int key) {
std::lock_guard lock(internal::KeypadLock());
if (auto future = futures_.PopIfAvailable()) {
future->key_pressed_ = key;
future->core_.Wake();
}
}
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: {
if (!select_future_.is_pendable()) {
select_future_ = pw::async2::Select(coin_slot_.GetCoins(),
keypad_.WaitForKeyPress());
}
PW_TRY_READY_ASSIGN(auto result, select_future_.Pend(cx));
if (result.has_value<0>()) {
unsigned coins = result.value<0>();
PW_LOG_INFO("Received %u coin%s.", coins, coins > 1 ? "s" : "");
PW_LOG_INFO("Please press a keypad key.");
coins_inserted_ += coins;
state_ = kAwaitingSelection;
} else {
PW_LOG_ERROR("Please insert a coin before making a selection.");
}
break;
}
case kAwaitingSelection: {
if (!select_future_.is_pendable()) {
select_future_ = pw::async2::Select(coin_slot_.GetCoins(),
keypad_.WaitForKeyPress());
}
PW_TRY_READY_ASSIGN(auto result, select_future_.Pend(cx));
if (result.has_value<1>()) {
int key = result.value<1>();
PW_LOG_INFO("Keypad %d was pressed. Dispensing an item.", key);
return pw::async2::Ready();
}
// If we didn't get a key press, we must have gotten a coin.
coins_inserted_ += result.value<0>();
PW_LOG_INFO("Received a coin. Your balance is currently %u coins.",
coins_inserted_);
PW_LOG_INFO("Please press a keypad key.");
break;
}
}
}
}
} // namespace codelab
#pragma once
#include <optional>
#include <utility>
#include "coin_slot.h"
#include "pw_async2/context.h"
#include "pw_async2/future.h"
#include "pw_async2/poll.h"
#include "pw_async2/select.h"
#include "pw_async2/task.h"
#include "pw_sync/interrupt_spin_lock.h"
#include "pw_sync/lock_annotations.h"
namespace codelab {
namespace internal {
inline pw::sync::InterruptSpinLock& KeypadLock() {
PW_CONSTINIT static pw::sync::InterruptSpinLock lock;
return lock;
}
} // namespace internal
class Keypad;
class KeyPressFuture {
public:
// The type returned by the future when it completes.
using value_type = int;
KeyPressFuture() = default;
KeyPressFuture(KeyPressFuture&& other) = default;
KeyPressFuture& operator=(KeyPressFuture&& other) = default;
~KeyPressFuture();
// Pends until a key is pressed, returning the key number.
pw::async2::Poll<value_type> Pend(pw::async2::Context& cx);
[[nodiscard]] bool is_pendable() const { return core_.is_pendable(); }
[[nodiscard]] bool is_complete() const { return core_.is_complete(); }
private:
friend class Keypad;
friend class pw::async2::FutureCore;
static constexpr const char kWaitReason[] = "Waiting for keypad press";
explicit KeyPressFuture(pw::async2::FutureState::Pending)
: core_(pw::async2::FutureState::kPending) {}
pw::async2::Poll<value_type> DoPend(pw::async2::Context& cx);
pw::async2::FutureCore core_;
// When present, holds the key that was pressed.
// If absent, the future is still pending.
std::optional<int> key_pressed_ PW_GUARDED_BY(internal::KeypadLock());
};
// Ensure that KeyPressFuture satisfies the Future concept.
static_assert(pw::async2::Future<KeyPressFuture>);
class Keypad {
public:
// Returns a future that resolves when a key is pressed with the value
// of the key.
//
// May only be called by one task.
KeyPressFuture WaitForKeyPress();
// Record a key press. Typically called from the keypad ISR.
void Press(int key);
private:
friend class KeyPressFuture;
// The list of futures waiting for a key press.
pw::async2::FutureList<&KeyPressFuture::core_> futures_
PW_GUARDED_BY(internal::KeypadLock());
};
// 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:
// 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;
CoinSlot& coin_slot_;
Keypad& keypad_;
pw::async2::SelectFuture<CoinFuture, KeyPressFuture> select_future_;
unsigned coins_inserted_;
enum State {
kWelcome,
kAwaitingPayment,
kAwaitingSelection,
};
State state_;
};
} // namespace codelab