5. Communicate between tasks#

pw_async2: Cooperative async tasks for embedded

Your vending machine is ready to handle more functionality. In this step, you’ll write code to handle a dispenser mechanism. The 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 notify the new DispenserTask about which items to dispense, and DispenserTask will send confirmation back.

Set up the item drop sensor#

The ItemDropSensor class that we’ve provided is similar to the CoinSlot and Keypad classes.

  1. Integrate the item drop sensor into main.cc:

    • Include item_drop_sensor.h

    • Create a global codelab::ItemDropSensor item_drop_sensor instance

    • Call the item drop sensor’s Drop() method in the interrupt handler (item_drop_sensor_isr())

    When an item is dispensed successfully, the item drop sensor triggers an interrupt, which is handled by the item_drop_sensor_isr() function.

    Hint
     1#include "coin_slot.h"
     2#include "hardware.h"
     3#include "item_drop_sensor.h"
     4#include "pw_async2/dispatcher.h"
     5#include "vending_machine.h"
     6
     7namespace {
     8
     9codelab::CoinSlot coin_slot;
    10codelab::ItemDropSensor item_drop_sensor;
    11codelab::Keypad keypad;
    12
    13}  // namespace
    14
    15// Interrupt handler function invoked when the user inserts a coin into the
    16// vending machine.
    17void coin_inserted_isr() { coin_slot.Deposit(); }
    18
    19// Interrupt handler function invoked when the user presses a key on the
    20// machine's keypad. Receives the value of the pressed key (0-9).
    21void key_press_isr(int key) { keypad.Press(key); }
    22
    23// Interrupt handler function invoked to simulate the item drop detector
    24// detecting confirmation that an item was successfully dispensed from the
    25// machine.
    26void item_drop_sensor_isr() { item_drop_sensor.Drop(); }
    

Set up communication channels#

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 a one-deep pw::InlineAsyncQueue to send events between the two tasks.

  1. Set up the queues in vending_machine.h:

    • Include pw_containers/inline_async_queue.h

    • Create a DispenseRequestQueue alias for pw::InlineAsyncQueue<int, 1> and a DispenseResponseQueue alias for pw::InlineAsyncQueue<bool, 1>

    You’ll use DispenseRequestQueue to send dispense requests (item numbers) from VendingMachineTask to DispenserTask. DispenseResponseQueue is for sending dispense responses (success or failure) from DispenserTask to VendingMachineTask.

    Hint
     1#include <optional>
     2
     3#include "coin_slot.h"
     4#include "item_drop_sensor.h"
     5#include "pw_async2/context.h"
     6#include "pw_async2/poll.h"
     7#include "pw_async2/task.h"
     8#include "pw_async2/waker.h"
     9#include "pw_containers/inline_async_queue.h"
    10#include "pw_sync/interrupt_spin_lock.h"
    11#include "pw_sync/lock_annotations.h"
    12
    13namespace codelab {
    14
    15using DispenseRequestQueue = pw::InlineAsyncQueue<int, 1>;
    16using DispenseResponseQueue = pw::InlineAsyncQueue<bool, 1>;
    
  2. Back in main.cc, set up the queues:

    • Declare a dispense_requests queue and a dispense_response queue

    • Provide the queues to VendingMachineTask when the task is created

    • Create a DispenserTask instance (we’ll implement this in the next section)

    • Post the new DispenserTask to the dispatcher

    Hint
     1int main() {
     2  pw::async2::Dispatcher dispatcher;
     3  codelab::HardwareInit(&dispatcher);
     4
     5  codelab::DispenseRequestQueue dispense_requests;
     6  codelab::DispenseResponseQueue dispense_responses;
     7
     8  codelab::VendingMachineTask task(
     9      coin_slot, keypad, dispense_requests, dispense_responses);
    10  dispatcher.Post(task);
    11
    12  codelab::DispenserTask dispenser_task(
    13      item_drop_sensor, dispense_requests, dispense_responses);
    14  dispatcher.Post(dispenser_task);
    15
    16  dispatcher.RunToCompletion();
    17
    18  return 0;
    19}
    

Create the new dispenser task#

The DispenserTask will turn the dispenser motor on and off in response to dispense requests from the VendingMachineTask.

  1. Declare a new DispenserTask class in vending_machine.h:

    • The DispenserTask constructor should accept references to the drop sensor and both comms queues as args

    • Create a State enum member with these states:

      • kIdle: Waiting for a dispense request (motor is off)

      • kDispensing: Actively dispensing an item (motor is on)

      • kReportDispenseSuccess: Waiting to report success (motor is off)

    • Create a data member to store the current state

    Hint
     1class DispenserTask : public pw::async2::Task {
     2 public:
     3  DispenserTask(ItemDropSensor& item_drop_sensor,
     4                DispenseRequestQueue& dispense_requests,
     5                DispenseResponseQueue& dispense_responses)
     6      : pw::async2::Task(PW_ASYNC_TASK_NAME("DispenserTask")),
     7        item_drop_sensor_(item_drop_sensor),
     8        dispense_requests_(dispense_requests),
     9        dispense_responses_(dispense_responses),
    10        state_{kIdle} {}
    11
    12 private:
    13  enum State {
    14    kIdle,
    15    kDispensing,
    16    kReportDispenseSuccess,
    17  };
    18
    19  pw::async2::Poll<> DoPend(pw::async2::Context& cx) override;
    20
    21  ItemDropSensor& item_drop_sensor_;
    22  DispenseRequestQueue& dispense_requests_;
    23  DispenseResponseQueue& dispense_responses_;
    24  State state_;
    25};
    
  2. Implement the dispenser’s state machine in vending_machine.cc:

    • Handle the kIdle, kDispensing, and kReportDispenseSuccess states (as well as the transitions between them)

    • Use the SetDispenserMotorState function that’s provided in hardware.h to control the dispenser’s motor

    Note

    Dispensing can’t fail yet. We’ll get to that later.

    Hint
     1pw::async2::Poll<> DispenserTask::DoPend(pw::async2::Context& cx) {
     2  PW_LOG_INFO("Dispenser task awake");
     3  while (true) {
     4    switch (state_) {
     5      case kIdle: {
     6        // Wait until a purchase is made.
     7        PW_TRY_READY(dispense_requests_.PendNotEmpty(cx));
     8
     9        // Clear any previously latched item drops.
    10        item_drop_sensor_.Clear();
    11
    12        // Start the motor to dispense the requested item.
    13        SetDispenserMotorState(dispense_requests_.front(), MotorState::kOn);
    14
    15        state_ = kDispensing;
    16        break;
    17      }
    18      case kDispensing: {
    19        // Wait for the item to drop.
    20        PW_TRY_READY(item_drop_sensor_.Pend(cx));
    21
    22        // Finished with this dispense request.
    23        SetDispenserMotorState(dispense_requests_.front(), MotorState::kOff);
    24        // Remove the last dispense request from the queue.
    25        dispense_requests_.pop();
    26
    27        state_ = kReportDispenseSuccess;
    28        break;
    29      }
    30      case kReportDispenseSuccess:
    31        // Wait for the response queue to have space.
    32        PW_TRY_READY(dispense_responses_.PendHasSpace(cx));
    33
    34        // Notify the vending task that an item was successfully dispensed.
    35        dispense_responses_.push(true);
    36
    37        state_ = kIdle;
    38        break;
    39    }
    40  }
    41}
    

Communicate between tasks#

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.

  1. Prepare VendingMachineTask for comms in vending_machine.h:

    • Add the communication queues as parameters to the VendingMachineTask constructor

    • Add new states: kAwaitingDispenseIdle (dispenser is ready for a request) and kAwaitingDispense (waiting for dispenser to finish dispensing an item)

    • Add data members for storing the communication queues

    Hint
     1class VendingMachineTask : public pw::async2::Task {
     2 public:
     3  VendingMachineTask(CoinSlot& coin_slot,
     4                     Keypad& keypad,
     5                     DispenseRequestQueue& dispense_requests,
     6                     DispenseResponseQueue& dispense_responses)
     7      : pw::async2::Task(PW_ASYNC_TASK_NAME("VendingMachineTask")),
     8        coin_slot_(coin_slot),
     9        keypad_(keypad),
    10        coins_inserted_(0),
    11        state_(kWelcome),
    12        dispense_requests_(dispense_requests),
    13        dispense_responses_(dispense_responses) {}
    14
    15 private:
    16  enum Input {
    17    kNone,
    18    kCoinInserted,
    19    kKeyPressed,
    20  };
    21
    22  // This is the core of the asynchronous task. The dispatcher calls this method
    23  // to give the task a chance to do work.
    24  pw::async2::Poll<> DoPend(pw::async2::Context& cx) override;
    25
    26  // Waits for either an inserted coin or keypress to occur, updating either
    27  // `coins_inserted_` or `selected_item_` accordingly.
    28  pw::async2::Poll<Input> PendInput(pw::async2::Context& cx);
    29
    30  CoinSlot& coin_slot_;
    31  Keypad& keypad_;
    32  unsigned coins_inserted_;
    33
    34  enum State {
    35    kWelcome,
    36    kAwaitingPayment,
    37    kAwaitingSelection,
    38    kAwaitingDispenseIdle,
    39    kAwaitingDispense,
    40  };
    41  State state_;
    42
    43  std::optional<int> selected_item_;
    44
    45  DispenseRequestQueue& dispense_requests_;
    46  DispenseResponseQueue& dispense_responses_;
    47};
    
  2. Update the vending machine task’s state machine in vending_machine.cc:

    • Transition the kAwaitingSelection state to kAwaitingDispenseIdle

    • Implement the kAwaitingDispenseIdle and kAwaitingDispense states

    Hint
     1      case kAwaitingSelection: {
     2        PW_TRY_READY_ASSIGN(Input input, PendInput(cx));
     3        switch (input) {
     4          case kCoinInserted:
     5            PW_LOG_INFO("Received a coin. Your balance is currently %u coins.",
     6                        coins_inserted_);
     7            PW_LOG_INFO("Press a keypad key to select an item.");
     8            break;
     9          case kKeyPressed:
    10            if (!selected_item_.has_value()) {
    11              state_ = kAwaitingSelection;
    12              continue;
    13            }
    14            PW_LOG_INFO("Keypad %d was pressed. Dispensing an item.",
    15                        selected_item_.value());
    16            // Pay for the item.
    17            coins_inserted_ = 0;
    18            state_ = kAwaitingDispenseIdle;
    19            break;
    20          case kNone:
    21            break;
    22        }
    23        break;
    24      }
    25
    26      case kAwaitingDispenseIdle: {
    27        PW_TRY_READY(dispense_requests_.PendHasSpace(cx));
    28        dispense_requests_.push(*selected_item_);
    29        state_ = kAwaitingDispense;
    30        break;
    31      }
    32
    33      case kAwaitingDispense: {
    34        PW_TRY_READY(dispense_responses_.PendNotEmpty(cx));
    35        const bool dispensed = dispense_responses_.front();
    36        dispense_responses_.pop();
    37
    38        if (dispensed) {
    39          // Accept the inserted money as payment
    40          PW_LOG_INFO("Dispense succeeded. Thanks for your purchase!");
    41          coins_inserted_ = 0;
    42          state_ = kWelcome;
    43        } else {
    44          PW_LOG_INFO("Dispense failed. Choose another selection.");
    45          state_ = kAwaitingSelection;
    46        }
    47        break;
    48      }
    49    }
    50  }
    51
    52  PW_UNREACHABLE;
    53}
    

Test the dispenser#

  1. Run the app:

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

  3. Press 1 Enter to make a selection.

  4. Press i Enter to trigger the item drop sensor, signaling that the item has finished dispensing.

    You should see the vending machine display it’s welcome message again.

    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  Dispenser task awake
    INF  [Motor for item 1 set to On]
    i
    INF  Dispenser task awake
    INF  [Motor for item 1 set to Off]
    INF  Dispense succeeded. Thanks for your purchase!
    INF  Welcome to the Pigweed Vending Machine!
    INF  Please insert a coin.
    

Congratulations! You now have a fully functioning vending machine! Or do you…?

Handle unexpected situations with timeouts#

What if you press the wrong button and accidentally buy an out-of-stock item? 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 TimeFuture.

  1. Prepare DispenserTask to support timeouts in vending_machine.h:

    • Include the headers that provide timeout-related features:

      • pw_async2/system_time_provider.h

      • pw_async2/time_provider.h

      • pw_chrono/system_clock.h

      Hint
       1#pragma once
       2
       3#include <optional>
       4
       5#include "coin_slot.h"
       6#include "item_drop_sensor.h"
       7#include "pw_async2/context.h"
       8#include "pw_async2/poll.h"
       9#include "pw_async2/system_time_provider.h"
      10#include "pw_async2/task.h"
      11#include "pw_async2/time_provider.h"
      12#include "pw_async2/waker.h"
      13#include "pw_chrono/system_clock.h"
      14#include "pw_containers/inline_async_queue.h"
      15#include "pw_sync/interrupt_spin_lock.h"
      16#include "pw_sync/lock_annotations.h"
      
    • Create a new kReportDispenseFailure state to represent dispense failures, a new kDispenseTimeout data member in DispenserTask that holds the timeout duration (std::chrono::seconds(5) is a good value), and a pw::async2::TimeFuture<pw::chrono::SystemClock> timeout_future_ member for holding the timeout future:

      Hint
       1class DispenserTask : public pw::async2::Task {
       2 public:
       3  DispenserTask(ItemDropSensor& item_drop_sensor,
       4                DispenseRequestQueue& dispense_requests,
       5                DispenseResponseQueue& dispense_responses)
       6      : pw::async2::Task(PW_ASYNC_TASK_NAME("DispenserTask")),
       7        item_drop_sensor_(item_drop_sensor),
       8        dispense_requests_(dispense_requests),
       9        dispense_responses_(dispense_responses),
      10        state_{kIdle} {}
      11
      12 private:
      13  enum State {
      14    kIdle,
      15    kDispensing,
      16    kReportDispenseSuccess,
      17    kReportDispenseFailure,
      18  };
      19
      20  pw::async2::Poll<> DoPend(pw::async2::Context& cx) override;
      21
      22  ItemDropSensor& item_drop_sensor_;
      23  DispenseRequestQueue& dispense_requests_;
      24  DispenseResponseQueue& dispense_responses_;
      25  State state_;
      26
      27  static constexpr auto kDispenseTimeout = std::chrono::seconds(5);
      28  pw::async2::TimeFuture<pw::chrono::SystemClock> timeout_future_;
      29};
      

      For testing purposes, make sure that the timeout period is long enough for a human to respond.

  2. Implement the timeout support in vending_machine.cc:

    • When you start dispensing an item (in your transition from kIdle to kDispensing), initialize the TimeFuture to your timeout.

    • 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:

      • If the item drop interrupt arrives first, clear the timeout with timeout_future_ = {}. If the timer isn’t cleared, it will fire later and wake DispenserTask unnecessarily, wasting time and power. After that, proceed to the kReportDispenseSuccess state.

      • If the timeout arrives first, proceed to the kReportDispenseFailure state.

      • In either case, be sure to turn off the motor and pop() the dispense request from the queue.

    • Handle the dispense failure state.

    Hint
     1pw::async2::Poll<> DispenserTask::DoPend(pw::async2::Context& cx) {
     2  PW_LOG_INFO("Dispenser task awake");
     3  while (true) {
     4    switch (state_) {
     5      case kIdle: {
     6        // Wait until a purchase is made.
     7        PW_TRY_READY(dispense_requests_.PendNotEmpty(cx));
     8
     9        // Clear any previously latched item drops.
    10        item_drop_sensor_.Clear();
    11
    12        // Start the motor to dispense the requested item.
    13        SetDispenserMotorState(dispense_requests_.front(), MotorState::kOn);
    14
    15        const auto expected_completion =
    16            pw::chrono::SystemClock::TimePointAfterAtLeast(kDispenseTimeout);
    17        timeout_future_ =
    18            pw::async2::GetSystemTimeProvider().WaitUntil(expected_completion);
    19
    20        state_ = kDispensing;
    21        break;
    22      }
    23      case kDispensing: {
    24        PW_TRY_READY_ASSIGN(
    25            auto result,
    26            pw::async2::Select(
    27                cx,
    28                pw::async2::PendableFor<&ItemDropSensor::Pend>(
    29                    item_drop_sensor_),
    30                pw::async2::PendableFor<
    31                    &pw::async2::TimeFuture<pw::chrono::SystemClock>::Pend>(
    32                    timeout_future_)));
    33
    34        // Finished with this dispense request.
    35        SetDispenserMotorState(dispense_requests_.front(), MotorState::kOff);
    36        dispense_requests_.pop();
    37
    38        // Check if the item dispensed successfully or not.
    39        pw::async2::VisitSelectResult(
    40            result,
    41            [](pw::async2::AllPendablesCompleted) {},
    42            [&](pw::async2::ReadyType) {
    43              // Clear the timeout. This releases the future's waker, preventing
    44              // an unnecessary wakeup when the timeout completes.
    45              timeout_future_ = {};
    46              state_ = kReportDispenseSuccess;
    47            },
    48            [&](std::chrono::time_point<pw::chrono::SystemClock>) {
    49              state_ = kReportDispenseFailure;
    50            });
    51        break;
    52      }
    53      case kReportDispenseSuccess:
    54        // Wait for the response queue to have space.
    55        PW_TRY_READY(dispense_responses_.PendHasSpace(cx));
    56
    57        // Notify the vending task that an item was successfully dispensed.
    58        dispense_responses_.push(true);
    59
    60        state_ = kIdle;
    61        break;
    62      case kReportDispenseFailure:
    63        PW_TRY_READY(dispense_responses_.PendHasSpace(cx));
    64        dispense_responses_.push(false);
    65        state_ = kIdle;
    66        break;
    67    }
    68  }
    69}
    70
    71}  // namespace codelab
    

Test the dispenser with timeouts#

  1. Run the app:

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

# Press 1 Enter to make a selection.

  1. Wait for the timeout.

    After 5 seconds you should see a message about the dispense failing.

    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.
    
  2. Try again, but this time press i Enter in 5 seconds or less so that the dispense succeeds.

Next steps#

Congratulations! You completed the codelab. Click DISPENSE PRIZE to retrieve your prize.

You now have a solid foundation in pw_async2 concepts, and quite a bit of hands-on experience with the framework. Try building something yourself with pw_async2! As always, if you get stuck, or if anything is unclear, or you just want to run some ideas by us, we would be happy to chat.

Checkpoint#

At this point, your code should look similar to the files below.

#include "coin_slot.h"
#include "hardware.h"
#include "item_drop_sensor.h"
#include "pw_async2/dispatcher.h"
#include "vending_machine.h"

namespace {

codelab::CoinSlot coin_slot;
codelab::ItemDropSensor item_drop_sensor;
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() { item_drop_sensor.Drop(); }

int main() {
  pw::async2::Dispatcher dispatcher;
  codelab::HardwareInit(&dispatcher);

  codelab::DispenseRequestQueue dispense_requests;
  codelab::DispenseResponseQueue dispense_responses;

  codelab::VendingMachineTask task(
      coin_slot, keypad, dispense_requests, dispense_responses);
  dispatcher.Post(task);

  codelab::DispenserTask dispenser_task(
      item_drop_sensor, dispense_requests, dispense_responses);
  dispatcher.Post(dispenser_task);

  dispatcher.RunToCompletion();

  return 0;
}
#include "vending_machine.h"

#include "hardware.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;
            state_ = kAwaitingDispenseIdle;
            break;
          case kNone:
            break;
        }
        break;
      }

      case kAwaitingDispenseIdle: {
        PW_TRY_READY(dispense_requests_.PendHasSpace(cx));
        dispense_requests_.push(*selected_item_);
        state_ = kAwaitingDispense;
        break;
      }

      case kAwaitingDispense: {
        PW_TRY_READY(dispense_responses_.PendNotEmpty(cx));
        const bool dispensed = dispense_responses_.front();
        dispense_responses_.pop();

        if (dispensed) {
          // Accept the inserted money as payment
          PW_LOG_INFO("Dispense succeeded. Thanks for your purchase!");
          coins_inserted_ = 0;
          state_ = kWelcome;
        } else {
          PW_LOG_INFO("Dispense failed. Choose another selection.");
          state_ = kAwaitingSelection;
        }
        break;
      }
    }
  }

  PW_UNREACHABLE;
}

pw::async2::Poll<> DispenserTask::DoPend(pw::async2::Context& cx) {
  PW_LOG_INFO("Dispenser task awake");
  while (true) {
    switch (state_) {
      case kIdle: {
        // Wait until a purchase is made.
        PW_TRY_READY(dispense_requests_.PendNotEmpty(cx));

        // Clear any previously latched item drops.
        item_drop_sensor_.Clear();

        // Start the motor to dispense the requested item.
        SetDispenserMotorState(dispense_requests_.front(), MotorState::kOn);

        const auto expected_completion =
            pw::chrono::SystemClock::TimePointAfterAtLeast(kDispenseTimeout);
        timeout_future_ =
            pw::async2::GetSystemTimeProvider().WaitUntil(expected_completion);

        state_ = kDispensing;
        break;
      }
      case kDispensing: {
        PW_TRY_READY_ASSIGN(
            auto result,
            pw::async2::Select(
                cx,
                pw::async2::PendableFor<&ItemDropSensor::Pend>(
                    item_drop_sensor_),
                pw::async2::PendableFor<
                    &pw::async2::TimeFuture<pw::chrono::SystemClock>::Pend>(
                    timeout_future_)));

        // Finished with this dispense request.
        SetDispenserMotorState(dispense_requests_.front(), MotorState::kOff);
        dispense_requests_.pop();

        // Check if the item dispensed successfully or not.
        pw::async2::VisitSelectResult(
            result,
            [](pw::async2::AllPendablesCompleted) {},
            [&](pw::async2::ReadyType) {
              // Clear the timeout. This releases the future's waker, preventing
              // an unnecessary wakeup when the timeout completes.
              timeout_future_ = {};
              state_ = kReportDispenseSuccess;
            },
            [&](std::chrono::time_point<pw::chrono::SystemClock>) {
              state_ = kReportDispenseFailure;
            });
        break;
      }
      case kReportDispenseSuccess:
        // Wait for the response queue to have space.
        PW_TRY_READY(dispense_responses_.PendHasSpace(cx));

        // Notify the vending task that an item was successfully dispensed.
        dispense_responses_.push(true);

        state_ = kIdle;
        break;
      case kReportDispenseFailure:
        PW_TRY_READY(dispense_responses_.PendHasSpace(cx));
        dispense_responses_.push(false);
        state_ = kIdle;
        break;
    }
  }
}

}  // namespace codelab
#pragma once

#include <optional>

#include "coin_slot.h"
#include "item_drop_sensor.h"
#include "pw_async2/context.h"
#include "pw_async2/poll.h"
#include "pw_async2/system_time_provider.h"
#include "pw_async2/task.h"
#include "pw_async2/time_provider.h"
#include "pw_async2/waker.h"
#include "pw_chrono/system_clock.h"
#include "pw_containers/inline_async_queue.h"
#include "pw_sync/interrupt_spin_lock.h"
#include "pw_sync/lock_annotations.h"

namespace codelab {

using DispenseRequestQueue = pw::InlineAsyncQueue<int, 1>;
using DispenseResponseQueue = pw::InlineAsyncQueue<bool, 1>;

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,
                     DispenseRequestQueue& dispense_requests,
                     DispenseResponseQueue& dispense_responses)
      : pw::async2::Task(PW_ASYNC_TASK_NAME("VendingMachineTask")),
        coin_slot_(coin_slot),
        keypad_(keypad),
        coins_inserted_(0),
        state_(kWelcome),
        dispense_requests_(dispense_requests),
        dispense_responses_(dispense_responses) {}

 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,
    kAwaitingDispenseIdle,
    kAwaitingDispense,
  };
  State state_;

  std::optional<int> selected_item_;

  DispenseRequestQueue& dispense_requests_;
  DispenseResponseQueue& dispense_responses_;
};

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:
  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_;

  static constexpr auto kDispenseTimeout = std::chrono::seconds(5);
  pw::async2::TimeFuture<pw::chrono::SystemClock> timeout_future_;
};

}  // namespace codelab