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#

  1. Set up the state machine data in vending_machine.h:

    • Declare a private State enum in VendingMachineTask that represents all possible states:

      • kWelcome

      • kAwaitingPayment

      • kAwaitingSelection

    • Declare a private State state_ data member that stores the current state

    • Initialize state_ in the VendingMachineTask constructor

    • Remove 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};
    
  2. Refactor the DoPend() implementation in vending_machine.cc as an explicit state machine:

    • Handle the kWelcome, kAwaitingPayment, and kAwaitingSelection 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:

  1. 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.

  1. Set up the Input enum in vending_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> header

    • Add 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};
    
  2. Update the VendingMachineTask implementation in vending_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 use Input:

       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