3. Create and wake a Future#

pw_async2: Cooperative async tasks for embedded

Currently, your vending machine immediately dispenses an item after receiving coins. In this step, you modify your app to let customers choose what to buy. 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 Future yourself.

In practice, you would not write a custom Future for such a common case as this. Pigweed provides ValueFuture for the purpose of returning a single async value. At the end of this step, we will use FutureCore to simplify our custom Future implementation.

Stub the Keypad class#

The hardware simulator sends you keypad events via async calls to key_press_isr(). The simulator sends integer values between 0 and 9 that indicate which keypad button was pressed. Your mission is to process these keypad events safely and to allow your task to wait for keypad numbers after receiving coins.

  1. Use the keypad in main.cc:

    • Declare a global keypad instance

    • Update key_press_isr() to handle keypad input events by invoking keypad.Press(key)

    • Pass a reference to the keypad when creating your task instance

    Hint
     1namespace {
     2
     3codelab::CoinSlot coin_slot;
     4codelab::Keypad keypad;
     5
     6}  // namespace
     7
     8// Interrupt handler function invoked when the user inserts a coin into the
     9// vending machine.
    10void coin_inserted_isr() { coin_slot.Deposit(); }
    11
    12// Interrupt handler function invoked when the user presses a key on the
    13// machine's keypad. Receives the value of the pressed key (0-9).
    14void key_press_isr(int key) { keypad.Press(key); }
    15
    16// Interrupt handler function invoked to simulate the item drop detector
    17// detecting confirmation that an item was successfully dispensed from the
    18// machine.
    19void item_drop_sensor_isr() {
    20  // In Step 5 you will uses this as part of a new Dispense task that runs
    21  // the dispenser motor until an item drops, or you time out on the vend
    22  // operation.
    23}
    24
    25int main() {
    26  pw::async2::BasicDispatcher dispatcher;
    27  codelab::HardwareInit(&dispatcher);
    28
    29  codelab::VendingMachineTask task(coin_slot, keypad);
    30  dispatcher.Post(task);
    31
    32  dispatcher.RunToCompletion();
    33
    34  return 0;
    35}
    
  2. Set up the Keypad and KeyPressFuture classes in vending_machine.h:

    • Declare the stub Keypad and KeyPressFuture classes:

       1class Keypad {
       2 public:
       3  // Returns a future that resolves when a key is pressed with the value
       4  // of the key.
       5  //
       6  // May only be called by one task.
       7  KeyPressFuture WaitForKeyPress();
       8
       9  // Record a key press. Typically called from the keypad ISR.
      10  void Press(int key);
      11
      12 private:
      13  // The future that will be resolved when a key is pressed.
      14  KeyPressFuture* key_press_future_ = nullptr;
      15};
      16
      17class KeyPressFuture {
      18 public:
      19  // The type returned by the future when it completes.
      20  using value_type = int;
      21
      22  KeyPressFuture() : state_(kDefaultConstructed) {}
      23
      24  // Pends until a key is pressed, returning the key number.
      25  pw::async2::Poll<value_type> Pend(pw::async2::Context& cx);
      26
      27  bool is_pendable() const { return state_ == kPendable; }
      28  bool is_complete() const { return state_ == kCompleted; }
      29
      30 private:
      31  friend class Keypad;
      32
      33  // Stub constructor that initializes the future with a key press.
      34  // We will replace this later.
      35  explicit KeyPressFuture(int key_pressed)
      36      : state_(kPendable), key_pressed_(key_pressed) {}
      37
      38  // Possible states of the future.
      39  enum {
      40    kDefaultConstructed,
      41    kPendable,
      42    kCompleted,
      43  } state_;
      44
      45  // When present, holds the key that was pressed.
      46  // If absent, the future is still pending.
      47  std::optional<int> key_pressed_;
      48};
      49
      50// Ensure that KeyPressFuture satisfies the Future concept.
      51static_assert(pw::async2::Future<KeyPressFuture>);
      

      Futures are required to expose four members:

      • value_type: The type returned by the future when it completes

      • Pend(Context& cx): Pend until the future is ready

      • is_pendable(): Return true if the future is valid and has not yet completed (i.e. handed out by some async operation rather than default constructed)

      • is_complete(): Return true if the future completed and its value has been consumed

      The concept pw_async2::Future can be used to ensure that your type conforms to the Future interface.

    • Set up a private keypad_ and key_future_ member for VendingMachineTask

    • Add a Keypad& argument to the VendingMachineTask constructor parameters and use it to initialize the keypad_ member

    • Set up an unsigned coins_inserted_ data member in VendingMachineTask that tracks how many coins have been inserted and initialize coins_inserted_ to 0 in the constructor

    Hint
     1namespace codelab {
     2
     3class KeyPressFuture;
     4
     5class Keypad {
     6 public:
     7  // Returns a future that resolves when a key is pressed with the value
     8  // of the key.
     9  //
    10  // May only be called by one task.
    11  KeyPressFuture WaitForKeyPress();
    12
    13  // Record a key press. Typically called from the keypad ISR.
    14  void Press(int key);
    15
    16 private:
    17  // The future that will be resolved when a key is pressed.
    18  KeyPressFuture* key_press_future_ = nullptr;
    19};
    20
    21class KeyPressFuture {
    22 public:
    23  // The type returned by the future when it completes.
    24  using value_type = int;
    25
    26  KeyPressFuture() : state_(kDefaultConstructed) {}
    27
    28  // Pends until a key is pressed, returning the key number.
    29  pw::async2::Poll<value_type> Pend(pw::async2::Context& cx);
    30
    31  bool is_pendable() const { return state_ == kPendable; }
    32  bool is_complete() const { return state_ == kCompleted; }
    33
    34 private:
    35  friend class Keypad;
    36
    37  // Stub constructor that initializes the future with a key press.
    38  // We will replace this later.
    39  explicit KeyPressFuture(int key_pressed)
    40      : state_(kPendable), key_pressed_(key_pressed) {}
    41
    42  // Possible states of the future.
    43  enum {
    44    kDefaultConstructed,
    45    kPendable,
    46    kCompleted,
    47  } state_;
    48
    49  // When present, holds the key that was pressed.
    50  // If absent, the future is still pending.
    51  std::optional<int> key_pressed_;
    52};
    53
    54// Ensure that KeyPressFuture satisfies the Future concept.
    55static_assert(pw::async2::Future<KeyPressFuture>);
    56
    57// The main task that drives the vending machine.
    58class VendingMachineTask : public pw::async2::Task {
    59 public:
    60  VendingMachineTask(CoinSlot& coin_slot, Keypad& keypad)
    61      : pw::async2::Task(PW_ASYNC_TASK_NAME("VendingMachineTask")),
    62        coin_slot_(coin_slot),
    63        keypad_(keypad),
    64        coins_inserted_(0) {}
    65
    66 private:
    67  // This is the core of the asynchronous task. The dispatcher calls this method
    68  // to give the task a chance to do work.
    69  pw::async2::Poll<> DoPend(pw::async2::Context& cx) override;
    70
    71  CoinSlot& coin_slot_;
    72  CoinFuture coin_future_;
    73  Keypad& keypad_;
    74  KeyPressFuture key_future_;
    75  unsigned coins_inserted_;
    76};
    77
    78}  // namespace codelab
    
  3. Create a stub implementation and integrate it (as well as the coin count data member) in vending_machine.cc:

    • Create a stub KeyPressFuture implementation:

       1pw::async2::Poll<int> KeyPressFuture::Pend(pw::async2::Context& cx) {
       2  if (key_pressed_.has_value()) {
       3    return pw::async2::Ready(key_pressed_.value());
       4  }
       5  return pw::async2::Pending();
       6}
       7
       8KeyPressFuture Keypad::WaitForKeyPress() { return KeyPressFuture(-1); }
       9
      10void Keypad::Press(int key) {}
      

      In the Pend member function we return the value of the key if it exists, or Pending otherwise. WaitForKeyPress() always returns a future its value set to -1, so it completes immediately. We will fix that later.

    • Update VendingMachineTask::DoPend() to check if coins have already been inserted before it pends for coins (coin_future_.Pend(cx))

    • Obtain a KeyPressFuture and wait for keypad input by invoking key_future_.Pend(cx) after coins have been inserted

    Hint
     1pw::async2::Poll<int> KeyPressFuture::Pend(pw::async2::Context& cx) {
     2  if (key_pressed_.has_value()) {
     3    return pw::async2::Ready(key_pressed_.value());
     4  }
     5  return pw::async2::Pending();
     6}
     7
     8KeyPressFuture Keypad::WaitForKeyPress() { return KeyPressFuture(-1); }
     9
    10void Keypad::Press(int key) {}
    11
    12pw::async2::Poll<> VendingMachineTask::DoPend(pw::async2::Context& cx) {
    13  if (coins_inserted_ == 0) {
    14    if (!coin_future_.is_pendable()) {
    15      PW_LOG_INFO("Welcome to the Pigweed Vending Machine!");
    16      PW_LOG_INFO("Please insert a coin.");
    17      coin_future_ = coin_slot_.GetCoins();
    18    }
    19
    20    PW_TRY_READY_ASSIGN(unsigned coins, coin_future_.Pend(cx));
    21    PW_LOG_INFO("Received %u coin%s.", coins, coins > 1 ? "s" : "");
    22    PW_LOG_INFO("Please press a keypad key.");
    23    coins_inserted_ += coins;
    24  }
    25
    26  if (!key_future_.is_pendable()) {
    27    key_future_ = keypad_.WaitForKeyPress();
    28  }
    29
    30  PW_TRY_READY_ASSIGN(int key, key_future_.Pend(cx));
    31  PW_LOG_INFO("Keypad %d was pressed. Dispensing an item.", key);
    32
    33  return pw::async2::Ready();
    34}
    

Verify the stub implementation#

  1. Run the app:

    bazelisk run //pw_async2/codelab
    
  2. Press c Enter to insert a coin.

    You should see a log stating that -1 was pressed. This is expected since the KeyPad::Pend() 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.
    

Handle keypad events#

Now, let’s update our Keypad and KeyPressFuture implementations to actually handle key presses.

  1. Track the active KeyPressFuture in Keypad:

    In order to resolve a future when a key is pressed, they Keypad needs to be aware of the active future. However, as the future is owned by the caller, they can freely move it around. Therefore, we also need KeyPressFuture to keep a reference to its Keypad to update its pointer if moved.

    The full code for these operations is given below:

     1KeyPressFuture::KeyPressFuture(KeyPressFuture&& other) noexcept
     2    : state_(std::exchange(other.state_, kDefaultConstructed)),
     3      keypad_(std::exchange(other.keypad_, nullptr)),
     4      key_pressed_(std::exchange(other.key_pressed_, std::nullopt)) {
     5  std::lock_guard lock(internal::KeypadLock());
     6  if (keypad_ != nullptr) {
     7    if (keypad_->key_press_future_ == &other) {
     8      keypad_->key_press_future_ = this;
     9    }
    10  }
    11}
    12
    13KeyPressFuture& KeyPressFuture::operator=(KeyPressFuture&& other) noexcept {
    14  if (this != &other) {
    15    state_ = std::exchange(other.state_, kDefaultConstructed);
    16    keypad_ = std::exchange(other.keypad_, nullptr);
    17    key_pressed_ = std::exchange(other.key_pressed_, std::nullopt);
    18
    19    std::lock_guard lock(internal::KeypadLock());
    20    if (keypad_ != nullptr) {
    21      if (keypad_->key_press_future_ == &other) {
    22        keypad_->key_press_future_ = this;
    23      } else if (keypad_->key_press_future_ == this) {
    24        keypad_->key_press_future_ = nullptr;
    25      }
    26    }
    27  }
    28  return *this;
    29}
    

    Note

    You should rarely have to write these boilerplate move operations yourself as Pigweed provides utilities to handle this for you, which will be introduced in a later step.

    • Add a Keypad* pointer to KeyPressFuture and replace the stub value constructor with a constructor that accepts a Keypad*, setting the state to kInitialized and the new future as the keypad’s active future.

    Hint
    1  explicit KeyPressFuture(Keypad& keypad)
    2      : state_(kInitialized), keypad_(&keypad) {
    3    std::lock_guard lock(internal::KeypadLock());
    4    keypad_->key_press_future_ = this;
    5  }
    
    • Update WaitForKeyPress() to pass this to the KeyPressFuture constructor.

  2. Protect keypad data access in vending_machine.h:

    • Include the pw_sync/interrupt_spin_lock.h and pw_sync/lock_annotations.h headers

    • Define an internal KeypadLock function to return a reference to a static pw::sync::InterruptSpinLock.

      Note

      We use a global lock here to safely handle destructor ordering issues. This logic mimics pw::async2::ValueFuture, which uses a similar pattern. While we are building this manually for learning purposes, in real code you would just use ValueFuture directly, which handles this for you.

    • Guard key_press_future_ with the spin lock with PW_GUARDED_BY

    Since the keypad ISR is asynchronous, you’ll need to synchronize access to the stored event data. For this codelab, we use pw::sync::InterruptSpinLock which is safe to acquire from an ISR in production use. Alternatively you can use atomic operations.

    We use PW_GUARDED_BY to add a compile-time check to ensure that the protected key future is only accessed when the spin lock is held.

    Hint
     1#include <optional>
     2
     3#include "coin_slot.h"
     4#include "pw_async2/context.h"
     5#include "pw_async2/future.h"
     6#include "pw_async2/poll.h"
     7#include "pw_async2/task.h"
     8#include "pw_sync/interrupt_spin_lock.h"
     9#include "pw_sync/lock_annotations.h"
    10
    11namespace codelab {
    12namespace internal {
    13
    14inline pw::sync::InterruptSpinLock& KeypadLock() {
    15  static pw::sync::InterruptSpinLock lock;
    16  return lock;
    17}
    18
    19}  // namespace internal
    20
    21class KeyPressFuture;
    22
    23class Keypad {
    24 public:
    25  // Returns a future that resolves when a key is pressed with the value
    26  // of the key.
    27  //
    28  // May only be called by one task.
    29  KeyPressFuture WaitForKeyPress();
    30
    31  // Record a key press. Typically called from the keypad ISR.
    32  void Press(int key);
    33
    34 private:
    35  friend class KeyPressFuture;
    36
    37  // The future that will be resolved when a key is pressed.
    38  KeyPressFuture* key_press_future_ PW_GUARDED_BY(internal::KeypadLock()) =
    39      nullptr;
    40};
    
  3. Implement keypress handling in vending_machine.cc:

    • In Keypad::Press(), store the keypress data in the active future if one exists. Remember to hold the lock!

    Hint
     1pw::async2::Poll<int> KeyPressFuture::Pend(pw::async2::Context& cx) {
     2  if (key_pressed_.has_value()) {
     3    return pw::async2::Ready(key_pressed_.value());
     4  }
     5  return pw::async2::Pending();
     6}
     7
     8KeyPressFuture Keypad::WaitForKeyPress() { return KeyPressFuture(*this); }
     9
    10void Keypad::Press(int key) {
    11  std::lock_guard lock(internal::KeypadLock());
    12  if (key_press_future_ != nullptr) {
    13    key_press_future_->key_pressed_ = key;
    14  }
    15}
    

And we’re done… what could go wrong?

Test the keypad implementation#

  1. Run the app:

    bazelisk run //pw_async2/codelab
    
  2. Press c Enter to insert a coin.

    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/task.cc:186: PW_CHECK() or PW_DCHECK() FAILED!
    
      FAILED ASSERTION
    
        !wakers_.empty()
    
      FILE & LINE
    
        pw_async2/task.cc:186
    
      FUNCTION
    
        Task::RunResult pw::async2::Task::RunInDispatcher()
    
      MESSAGE
    
        Task VendingMachineTask:0x16b48e5b0 returned Pending() without registering a waker
    

    To prevent obviously incorrect usage, the pw_async2 module asserts if you return Pending() without actually storing a Waker, because it means your task has no way of being woken back up.

Store a waker#

In general, you should always store a Waker before returning Pending(). A waker is a lightweight object that allows you to tell the dispatcher to wake a task. When a Task::DoPend() call returns Pending(), the task is put to sleep so that the dispatcher doesn’t have to repeatedly poll the task. Without a waker, the task will sleep forever.

  1. Declare a waker in vending_machine.h:

    • Include the pw_async2/waker.h header

    • Add a pw::async2::Waker waker_ private member to the KeyPressFuture class

    Hint
     1#include <optional>
     2
     3#include "coin_slot.h"
     4#include "pw_async2/context.h"
     5#include "pw_async2/future.h"
     6#include "pw_async2/poll.h"
     7#include "pw_async2/task.h"
     8#include "pw_async2/waker.h"
     9#include "pw_sync/interrupt_spin_lock.h"
    10#include "pw_sync/lock_annotations.h"
    11
    12namespace codelab {
    13namespace internal {
    14
    15inline pw::sync::InterruptSpinLock& KeypadLock() {
    16  static pw::sync::InterruptSpinLock lock;
    17  return lock;
    18}
    19
    20}  // namespace internal
    21
    22class KeyPressFuture;
    23
    24class Keypad {
    25 public:
    26  // Returns a future that resolves when a key is pressed with the value
    27  // of the key.
    28  //
    29  // May only be called by one task.
    30  KeyPressFuture WaitForKeyPress();
    31
    32  // Record a key press. Typically called from the keypad ISR.
    33  void Press(int key);
    34
    35 private:
    36  friend class KeyPressFuture;
    37
    38  // The future that will be resolved when a key is pressed.
    39  KeyPressFuture* key_press_future_ PW_GUARDED_BY(internal::KeypadLock()) =
    40      nullptr;
    41};
    42
    43class KeyPressFuture {
    44 public:
    45  // The type returned by the future when it completes.
    46  using value_type = int;
    47
    48  KeyPressFuture() : state_(kDefaultConstructed) {}
    49
    50  KeyPressFuture(const KeyPressFuture&) = delete;
    51  KeyPressFuture& operator=(const KeyPressFuture&) = delete;
    52
    53  KeyPressFuture(KeyPressFuture&& other) noexcept;
    54  KeyPressFuture& operator=(KeyPressFuture&& other) noexcept;
    55
    56  // Pends until a key is pressed, returning the key number.
    57  pw::async2::Poll<value_type> Pend(pw::async2::Context& cx);
    58
    59  bool is_pendable() const { return state_ == kInitialized; }
    60  bool is_complete() const { return state_ == kCompleted; }
    61
    62 private:
    63  friend class Keypad;
    64
    65  explicit KeyPressFuture(Keypad& keypad)
    66      : state_(kInitialized), keypad_(&keypad) {
    67    std::lock_guard lock(internal::KeypadLock());
    68    keypad_->key_press_future_ = this;
    69  }
    70
    71  // Possible states of the future.
    72  enum {
    73    kDefaultConstructed,
    74    kInitialized,
    75    kCompleted,
    76  } state_;
    77
    78  Keypad* keypad_;
    79
    80  // When present, holds the key that was pressed.
    81  // If absent, the future is still pending.
    82  std::optional<int> key_pressed_;
    83
    84  pw::async2::Waker waker_;
    85};
    86
    87// Ensure that KeyPressFuture satisfies the Future concept.
    88static_assert(pw::async2::Future<KeyPressFuture>);
    
  2. Use the waker in vending_machine.cc:

    • In KeyPressFuture::Pend() store the waker before returning Pending():

      PW_ASYNC_STORE_WAKER(cx, waker_, "Waiting for keypad press");
      

    The last argument should always be a meaningful string describing the wait reason. In the next section you’ll see how this string can help you debug issues.

    Hint
     1pw::async2::Poll<int> KeyPressFuture::Pend(pw::async2::Context& cx) {
     2  if (key_pressed_.has_value()) {
     3    return pw::async2::Ready(key_pressed_.value());
     4  }
     5  PW_ASYNC_STORE_WAKER(cx, waker_, "Waiting for keypad press");
     6  return pw::async2::Pending();
     7}
     8
     9KeyPressFuture Keypad::WaitForKeyPress() { return KeyPressFuture(*this); }
    10
    11void Keypad::Press(int key) {
    12  std::lock_guard lock(internal::KeypadLock());
    13  if (key_press_future_ != nullptr) {
    14    key_press_future_->key_pressed_ = key;
    15  }
    16}
    

Tip

See Setting up wakers for an overview of all of the different ways that you can set up wakers.

Forget to wake the task#

You’ve set up the waker but you’re not using it yet. Let’s see what happens.

  1. Run the app:

    bazelisk run //pw_async2/codelab
    
  2. Press c Enter to insert a coin.

  3. Press 1 Enter to select an item.

    You should see output like this:

    INF  Welcome to the Pigweed Vending Machine!
    INF  Please insert a coin.
    c
    INF  Received 1 coin.
    INF  Please press a keypad key.
    1
    

    Nothing happens, not even an assertion! This time there is no crash! Why??

    Answer

    The problem is that pw_async2 has no way of knowing when the task is ready to be woken up.

    The reason pw_async2 can detect forgetting to store the waker, on the other hand, is because it happens during a pw_async2-initiated call into your code, so there can be a postcondition check.

  4. Debug this issue by pressing d Enter:

    You should see output like this:

    d
    INF  pw::async2::Dispatcher
    INF  Woken tasks:
    INF  Sleeping tasks:
    INF    - VendingMachineTask:0x7ffeec48fd90 (1 wakers)
    INF      * Waker 1: Waiting for keypad press
    

    This shows the state of all the tasks registered with the dispatcher. The last line is the wait reason string that you provided when you registered the waker. We can see that the vending machine task is still sleeping.

    Tip

    If you use pw_async2 in your own project, you can get this kind of debug information by calling LogRegisteredTasks.

    If you don’t see the reason messages, make sure that PW_ASYNC2_DEBUG_WAIT_REASON is not set to 0.

Wake the task#

  1. Fix the issue in vending_machine.cc:

    • When the keypad is pressed, invoke the Wake() method on the Waker:

      1void Keypad::Press(int key) {
      2  std::lock_guard lock(internal::KeypadLock());
      3  if (key_press_future_ != nullptr) {
      4    key_press_future_->key_pressed_ = key;
      5    key_press_future_->waker_.Wake();
      6  }
      

      By design, the Wake() call consumes the Waker, leaving it in an empty state. Additionally, wakers are default-constructed in an empty state, and moving the value means the location that is moved from is reset to an empty state. If you invoke Wake() on an empty Waker, the call is a no-op.

  2. Verify the fix:

    bazelisk run //pw_async2/codelab
    
  3. Press c Enter to insert a coin.

  4. Press 1 Enter to select an item.

    You should see keypad input working correctly now:

    INF  Welcome to the Pigweed Vending Machine!
    INF  Please insert a coin.
    c
    INF  Received 1 coin.
    INF  Please press a keypad key.
    1
    INF  Keypad 1 was pressed. Dispensing an item.
    

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. LogRegisteredTasks() can also help here by pointing to a problem related to waking your task.

Simplify with FutureCore#

The manual implementation of KeyPressFuture is complex and error-prone due to the need to manage move semantics and pointers between the future and the keypad, as well as having to manage a Waker.

Pigweed provides pw::async2::FutureCore to handle these details for you. FutureCore manages the listing of futures in a linked list owned by a future provider like the Keypad and automatically stores a Waker when returning Pending.

  1. Update vending_machine.h to use FutureCore:

    • Include pw_async2/future.h.

    • Replace the waker_ member in your custom KeyPressFuture with a pw::async2::FutureCore core_, then delete your custom move constructor and move assignment operator. The default move operations provided by the compiler will handle everything for you.

    • Make pw::async2::FutureCore a friend of KeyPressFuture.

    • Replace the key_press_future_ pointer in Keypad with a pw::async2::FutureList<&KeyPressFuture::core_> futures_. By templating it on a member pointer to a concrete future’s FutureCore, the FutureList can retrieve the KeyPressFuture itself.

    • To set the waker’s wait reason, define a static constexpr const char kWaitReason[] member in KeyPressFuture with the wait reason string.

    Hint
     1class KeyPressFuture {
     2 public:
     3  // The type returned by the future when it completes.
     4  using value_type = int;
     5
     6  KeyPressFuture() : core_(), keypad_(nullptr) {}
     7
     8  // Pends until a key is pressed, returning the key number.
     9  pw::async2::Poll<value_type> Pend(pw::async2::Context& cx);
    10
    11  [[nodiscard]] bool is_pendable() const { return core_.is_pendable(); }
    12  [[nodiscard]] bool is_complete() const { return core_.is_complete(); }
    13
    14 private:
    15  friend class Keypad;
    16  friend class pw::async2::FutureCore;
    17
    18  static constexpr const char kWaitReason[] = "Waiting for keypad press";
    19
    20  explicit KeyPressFuture(Keypad& keypad);
    21
    22  pw::async2::Poll<value_type> DoPend(pw::async2::Context& cx);
    23
    24  pw::async2::FutureCore core_;
    25  Keypad* keypad_;
    26
    27  // When present, holds the key that was pressed.
    28  // If absent, the future is still pending.
    29  std::optional<int> key_pressed_ PW_GUARDED_BY(internal::KeypadLock());
    30};
    31
    32// Ensure that KeyPressFuture satisfies the Future concept.
    33static_assert(pw::async2::Future<KeyPressFuture>);
    34
    35class Keypad {
    36 public:
    37  // Returns a future that resolves when a key is pressed with the value
    38  // of the key.
    39  //
    40  // May only be called by one task.
    41  KeyPressFuture WaitForKeyPress();
    42
    43  // Record a key press. Typically called from the keypad ISR.
    44  void Press(int key);
    45
    46 private:
    47  friend class KeyPressFuture;
    48
    49  // The list of futures waiting for a key press.
    50  pw::async2::FutureList<&KeyPressFuture::core_> futures_
    51      PW_GUARDED_BY(internal::KeypadLock());
    52};
    
  2. Update vending_machine.cc to implement the simplified logic:

    • Delete all the manual move logic.

    • In the KeyPressFuture constructor, initialize the list and push the future to the keypad’s futures_ list.

    • In Keypad::Press, use futures_.PopIfAvailable() to retrieve the active future, then set its key_pressed_ member and wake it.

    • Rename your KeyPressFuture::Pend to DoPend, then add a simple Pend method that delegates to core_.DoPend(*this, cx).

    Hint
     1KeyPressFuture::KeyPressFuture(Keypad& keypad)
     2    : core_(pw::async2::FutureState::kPending), keypad_(&keypad) {
     3  std::lock_guard lock(internal::KeypadLock());
     4  keypad_->futures_.Push(*this);
     5}
     6
     7pw::async2::Poll<int> KeyPressFuture::Pend(pw::async2::Context& cx) {
     8  return core_.DoPend(*this, cx);
     9}
    10
    11pw::async2::Poll<int> KeyPressFuture::DoPend(pw::async2::Context&) {
    12  std::lock_guard lock(internal::KeypadLock());
    13  if (key_pressed_.has_value()) {
    14    return pw::async2::Ready(*key_pressed_);
    15  }
    16  return pw::async2::Pending();
    17}
    18
    19KeyPressFuture Keypad::WaitForKeyPress() { return KeyPressFuture(*this); }
    20
    21void Keypad::Press(int key) {
    22  std::lock_guard lock(internal::KeypadLock());
    23  if (auto future = futures_.PopIfAvailable()) {
    24    future->key_pressed_ = key;
    25    future->core_.Wake();
    26  }
    27}
    

Next steps#

Continue to 4. Use a state machine to learn how to manage the rapidly increasing complexity of your code.

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 {

KeyPressFuture::KeyPressFuture(Keypad& keypad)
    : core_(pw::async2::FutureState::kPending), keypad_(&keypad) {
  std::lock_guard lock(internal::KeypadLock());
  keypad_->futures_.Push(*this);
}

pw::async2::Poll<int> KeyPressFuture::Pend(pw::async2::Context& cx) {
  return core_.DoPend(*this, cx);
}

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() { return KeyPressFuture(*this); }

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) {
  if (coins_inserted_ == 0) {
    if (!coin_future_.is_pendable()) {
      PW_LOG_INFO("Welcome to the Pigweed Vending Machine!");
      PW_LOG_INFO("Please insert a coin.");
      coin_future_ = coin_slot_.GetCoins();
    }

    PW_TRY_READY_ASSIGN(unsigned coins, coin_future_.Pend(cx));
    PW_LOG_INFO("Received %u coin%s.", coins, coins > 1 ? "s" : "");
    PW_LOG_INFO("Please press a keypad key.");
    coins_inserted_ += coins;
  }

  if (!key_future_.is_pendable()) {
    key_future_ = keypad_.WaitForKeyPress();
  }

  PW_TRY_READY_ASSIGN(int key, key_future_.Pend(cx));
  PW_LOG_INFO("Keypad %d was pressed. Dispensing an item.", key);

  return pw::async2::Ready();
}

}  // namespace codelab
#pragma once

#include <optional>

#include "coin_slot.h"
#include "pw_async2/context.h"
#include "pw_async2/future.h"
#include "pw_async2/poll.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() {
  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() : core_(), keypad_(nullptr) {}

  // 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(Keypad& keypad);

  pw::async2::Poll<value_type> DoPend(pw::async2::Context& cx);

  pw::async2::FutureCore core_;
  Keypad* keypad_;

  // 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) {}

 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_;
  CoinFuture coin_future_;
  Keypad& keypad_;
  KeyPressFuture key_future_;
  unsigned coins_inserted_;
};

}  // namespace codelab