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/basic_dispatcher.h"
     5#include "vending_machine.h"
     6
     7namespace {
     8
     9codelab::CoinSlot coin_slot;
    10codelab::Keypad keypad;
    11codelab::ItemDropSensor item_drop_sensor;
    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.

The mechanism through which async tasks communicate in pw_async2 is pw::async2::Channel. At its core, a channel is a asynchronous, threadsafe queue that can have one or more senders and one or more receivers.

For our vending machine, we’ll use two channels:

  • A Channel<int> for the vending machine to send dispense requests (item numbers) to the dispenser task.

  • A Channel<bool> for the dispenser task to send dispense responses (success or failure) back to the vending machine task.

  1. Update vending_machine.h to allow communications:

    • Include pw_async2/channel.h

    • Update your VendingMachineTask to accept and store a pw::async2::Sender<int> to send requests and a pw::async2::Receiver<bool> to receive responses.

    Hint
     1class VendingMachineTask : public pw::async2::Task {
     2 public:
     3  VendingMachineTask(CoinSlot& coin_slot,
     4                     Keypad& keypad,
     5                     pw::async2::Sender<int> dispense_requests,
     6                     pw::async2::Receiver<bool> dispense_responses)
     7      : pw::async2::Task(PW_ASYNC_TASK_NAME("VendingMachineTask")),
     8        coin_slot_(coin_slot),
     9        keypad_(keypad),
    10        dispense_requests_(std::move(dispense_requests)),
    11        dispense_responses_(std::move(dispense_responses)),
    12        coins_inserted_(0),
    13        state_(kWelcome) {}
    14
    15 private:
    16  // This is the core of the asynchronous task. The dispatcher calls this method
    17  // to give the task a chance to do work.
    18  pw::async2::Poll<> DoPend(pw::async2::Context& cx) override;
    19
    20  CoinSlot& coin_slot_;
    21  Keypad& keypad_;
    22  pw::async2::SelectFuture<CoinFuture, KeyPressFuture> select_future_;
    23  pw::async2::Sender<int> dispense_requests_;
    24  pw::async2::Receiver<bool> dispense_responses_;
    25  unsigned coins_inserted_;
    26  int item_to_dispense_;
    27
    28  enum State {
    29    kWelcome,
    30    kAwaitingPayment,
    31    kAwaitingSelection,
    32    kAwaitingDispenseIdle,
    33    kAwaitingDispense,
    34  };
    35  State state_;
    36};
    
  2. Create the channels in main.cc:

    • Define a local pw::async2::ChannelStorage<int, 1> dispense_requests and a pw::async2::ChannelStorage<bool, 1> dispense_responses in your main() function. These serve as the buffers for the channels.

      Note

      Channels optionally can dynamically allocate their storage, removing the need for a ChannelStorage declaration. Refer to the Channel docs for more details.

    • Create the channels themselves using the pw::async2::CreateSpscChannel() function. Spsc means “single producer, single consumer”, which is what we need to communicate between the two tasks.

      CreateSpscChannel returns a tuple of three items: a handle to the channel, a sender, and a receiver.

      The channel handle is used if you need to close the channel early, and to create additional senders and receivers if the channel supports it.

      Since we have an SPSC channel, no new senders or receivers can be created, and we don’t need to close our channels, we don’t need the handle here. To indicate this, we immediately call Release on the two handles we created, telling the channel that it should manage its lifetime automatically via its active senders and receivers.

    • Pass the sender for the requests channel and the receiver for the responses channel to the VendingMachineTask constructor

    • Create a DispenserTask instance (we’ll define this in the next section), passing the drop sensor and the opposite sender/receiver pair to its constructor

    • Post the new DispenserTask to the dispatcher

    Hint
     1int main() {
     2  pw::async2::BasicDispatcher dispatcher;
     3  codelab::HardwareInit(&dispatcher);
     4
     5  pw::async2::ChannelStorage<int, 1> dispense_requests;
     6  pw::async2::ChannelStorage<bool, 1> dispense_responses;
     7
     8  auto [req_handle, dispense_requests_sender, dispense_requests_receiver] =
     9      pw::async2::CreateSpscChannel(dispense_requests);
    10  auto [res_handle, dispense_responses_sender, dispense_responses_receiver] =
    11      pw::async2::CreateSpscChannel(dispense_responses);
    12  req_handle.Release();
    13  res_handle.Release();
    14
    15  codelab::VendingMachineTask task(coin_slot,
    16                                   keypad,
    17                                   std::move(dispense_requests_sender),
    18                                   std::move(dispense_responses_receiver));
    19  dispatcher.Post(task);
    20
    21  codelab::DispenserTask dispenser_task(item_drop_sensor,
    22                                        std::move(dispense_requests_receiver),
    23                                        std::move(dispense_responses_sender));
    24  dispatcher.Post(dispenser_task);
    25
    26  dispatcher.RunToCompletion();
    27
    28  return 0;
    29}
    

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, the requests channel receiver, and the responses channel sender 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

    • Remember to define your DoPend override

    Hint
     1class DispenserTask : public pw::async2::Task {
     2 public:
     3  DispenserTask(ItemDropSensor& drop_sensor,
     4                pw::async2::Receiver<int> dispense_requests,
     5                pw::async2::Sender<bool> dispense_responses)
     6      : pw::async2::Task(PW_ASYNC_TASK_NAME("DispenserTask")),
     7        drop_sensor_(drop_sensor),
     8        dispense_requests_(std::move(dispense_requests)),
     9        dispense_responses_(std::move(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& drop_sensor_;
    22  pw::async2::Receiver<int> dispense_requests_;
    23  pw::async2::Sender<bool> dispense_responses_;
    24  pw::async2::ReceiveFuture<int> dispense_request_future_;
    25  pw::async2::SendFuture<bool> dispense_response_future_;
    26  pw::async2::ValueFuture<void> drop_future_;
    27  std::optional<int> item_to_dispense_;
    28  State state_;
    29};
    
  2. Implement the dispenser’s state machine in vending_machine.cc:

    • In kIdle, call Receive to get a future for the next dispense request and pend it to completion, then turn on the dispenser motor using SetDispenserMotorState, which is provided in hardware.h.

    • In kDispensing, wait for the item drop sensor to trigger then turn off the dispenser motor and transition to kReportDispenseSuccess.

    • In kReportDispenseSuccess, use the response channel sender to send true to the VendingMachineTask and transition to kIdle.

    Note

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

    Hint
     1pw::async2::Poll<> DispenserTask::DoPend(pw::async2::Context& cx) {
     2  while (true) {
     3    switch (state_) {
     4      case kIdle: {
     5        if (!dispense_request_future_.is_pendable()) {
     6          dispense_request_future_ = dispense_requests_.Receive();
     7        }
     8        PW_TRY_READY_ASSIGN(item_to_dispense_,
     9                            dispense_request_future_.Pend(cx));
    10        if (!item_to_dispense_.has_value()) {
    11          PW_LOG_WARN("Dispense requests channel unexpectedly closed.");
    12          return pw::async2::Ready();
    13        }
    14        SetDispenserMotorState(*item_to_dispense_, MotorState::kOn);
    15        state_ = kDispensing;
    16        break;
    17      }
    18
    19      case kDispensing: {
    20        if (!drop_future_.is_pendable()) {
    21          drop_future_ = drop_sensor_.Wait();
    22        }
    23        PW_TRY_READY(drop_future_.Pend(cx));
    24        SetDispenserMotorState(*item_to_dispense_, MotorState::kOff);
    25        item_to_dispense_ = std::nullopt;
    26        state_ = kReportDispenseSuccess;
    27        break;
    28      }
    29
    30      case kReportDispenseSuccess: {
    31        if (!dispense_response_future_.is_pendable()) {
    32          dispense_response_future_ = dispense_responses_.Send(true);
    33        }
    34        PW_TRY_READY_ASSIGN(bool result, dispense_response_future_.Pend(cx));
    35        if (!result) {
    36          PW_LOG_WARN("Dispense responses channel unexpectedly closed.");
    37          return pw::async2::Ready();
    38        }
    39        state_ = kIdle;
    40        break;
    41      }
    42    }
    43  }
    44
    45  return pw::async2::Ready();
    46}
    

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 channel. Then it will wait for a response with the dispense responses channel.

  1. Prepare VendingMachineTask for comms in vending_machine.h:

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

    • Define member variables for the request send and response receive futures.

    Hint
     1class VendingMachineTask : public pw::async2::Task {
     2 public:
     3  VendingMachineTask(CoinSlot& coin_slot,
     4                     Keypad& keypad,
     5                     pw::async2::Sender<int> dispense_requests,
     6                     pw::async2::Receiver<bool> dispense_responses)
     7      : pw::async2::Task(PW_ASYNC_TASK_NAME("VendingMachineTask")),
     8        coin_slot_(coin_slot),
     9        keypad_(keypad),
    10        dispense_requests_(std::move(dispense_requests)),
    11        dispense_responses_(std::move(dispense_responses)),
    12        coins_inserted_(0),
    13        state_(kWelcome) {}
    14
    15 private:
    16  // This is the core of the asynchronous task. The dispatcher calls this method
    17  // to give the task a chance to do work.
    18  pw::async2::Poll<> DoPend(pw::async2::Context& cx) override;
    19
    20  CoinSlot& coin_slot_;
    21  Keypad& keypad_;
    22  pw::async2::SelectFuture<CoinFuture, KeyPressFuture> select_future_;
    23  pw::async2::Sender<int> dispense_requests_;
    24  pw::async2::Receiver<bool> dispense_responses_;
    25  pw::async2::SendFuture<int> dispense_request_future_;
    26  pw::async2::ReceiveFuture<bool> dispense_response_future_;
    27  unsigned coins_inserted_;
    28  int item_to_dispense_;
    29
    30  enum State {
    31    kWelcome,
    32    kAwaitingPayment,
    33    kAwaitingSelection,
    34    kAwaitingDispenseIdle,
    35    kAwaitingDispense,
    36  };
    37  State state_;
    38};
    
  2. Update the vending machine task’s state machine in vending_machine.cc:

    • Transition the kAwaitingSelection state to kAwaitingDispenseIdle, storing the selected item in a member variable.

    • Implement the kAwaitingDispenseIdle and kAwaitingDispense states:

      • In kAwaitingDispenseIdle, send the selected item to the DispenserTask using the dispense requests sender.

      • In kAwaitingDispense, wait for a response from the DispenserTask and log whether the dispense was successful.

        Once complete, you can return to kWelcome to start the process over.

    Hint
     1      case kAwaitingSelection: {
     2        if (!select_future_.is_pendable()) {
     3          select_future_ = pw::async2::Select(coin_slot_.GetCoins(),
     4                                              keypad_.WaitForKeyPress());
     5        }
     6        PW_TRY_READY_ASSIGN(auto result, select_future_.Pend(cx));
     7        if (result.has_value<1>()) {
     8          int key = result.value<1>();
     9          PW_LOG_INFO("Keypad %d was pressed. Dispensing an item.", key);
    10          state_ = kAwaitingDispenseIdle;
    11          break;
    12        }
    13
    14        coins_inserted_ += result.value<unsigned>();
    15        PW_LOG_INFO("Received a coin. Your balance is currently %u coins.",
    16                    coins_inserted_);
    17        PW_LOG_INFO("Please press a keypad key.");
    18        break;
    19      }
    20
    21      case kAwaitingDispenseIdle: {
    22        if (!dispense_request_future_.is_pendable()) {
    23          dispense_request_future_ = dispense_requests_.Send(item_to_dispense_);
    24        }
    25        PW_TRY_READY_ASSIGN(bool sent, dispense_request_future_.Pend(cx));
    26        if (!sent) {
    27          PW_LOG_WARN("Dispense requests channel unexpectedly closed.");
    28          return pw::async2::Ready();
    29        }
    30        state_ = kAwaitingDispense;
    31        break;
    32      }
    33
    34      case kAwaitingDispense: {
    35        if (!dispense_response_future_.is_pendable()) {
    36          dispense_response_future_ = dispense_responses_.Receive();
    37        }
    38        std::optional<bool> received;
    39        PW_TRY_READY_ASSIGN(received, dispense_response_future_.Pend(cx));
    40        if (!received.has_value()) {
    41          PW_LOG_WARN("Dispense responses channel unexpectedly closed.");
    42          return pw::async2::Ready();
    43        }
    44
    45        if (received.value()) {
    46          PW_LOG_INFO("Item dispensed successfully.");
    47          state_ = kWelcome;
    48        } else {
    49          PW_LOG_ERROR("Dispense failed. Choose another selection.");
    50          state_ = kAwaitingSelection;
    51        }
    52      }
    53    }
    54  }
    55}
    

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. Pigweed provides a Timeout helper which works with various common future types.

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

    • Include the header that provides timeout-related features:

      • pw_async2/future_timeout.h

      Hint
       1#pragma once
       2
       3#include <optional>
       4#include <utility>
       5
       6#include "coin_slot.h"
       7#include "item_drop_sensor.h"
       8#include "pw_async2/channel.h"
       9#include "pw_async2/context.h"
      10#include "pw_async2/future.h"
      11#include "pw_async2/future_timeout.h"
      12#include "pw_async2/poll.h"
      13#include "pw_async2/select.h"
      14#include "pw_async2/task.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 update the drop_future_ member to be a ValueFutureWithTimeout:

      Hint
       1class DispenserTask : public pw::async2::Task {
       2 public:
       3  DispenserTask(ItemDropSensor& drop_sensor,
       4                pw::async2::Receiver<int> dispense_requests,
       5                pw::async2::Sender<bool> dispense_responses)
       6      : pw::async2::Task(PW_ASYNC_TASK_NAME("DispenserTask")),
       7        drop_sensor_(drop_sensor),
       8        dispense_requests_(std::move(dispense_requests)),
       9        dispense_responses_(std::move(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& drop_sensor_;
      23  pw::async2::Receiver<int> dispense_requests_;
      24  pw::async2::Sender<bool> dispense_responses_;
      25  pw::async2::ReceiveFuture<int> dispense_request_future_;
      26  pw::async2::SendFuture<bool> dispense_response_future_;
      27  pw::async2::ValueFutureWithTimeout<void> drop_future_;
      28  std::optional<int> item_to_dispense_;
      29  State state_;
      30
      31  static constexpr auto kDispenseTimeout = std::chrono::seconds(5);
      32};
      

      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 drop_future_ using Timeout.

      Timeout takes the future to wrap and the timeout duration as arguments.

    • In the kDispensing state, pend the drop_future_.

    • Check the result of the future:

      • If the result is OK, then the item dropped successfully. Proceed to the kReportDispenseSuccess state.

      • If the result status is DeadlineExceeded, then the timeout occurred. Proceed to the kReportDispenseFailure state.

      • In either case, be sure to turn off the motor and clear the dispense request.

    • Handle the dispense failure state.

    Hint
     1pw::async2::Poll<> DispenserTask::DoPend(pw::async2::Context& cx) {
     2  while (true) {
     3    switch (state_) {
     4      case kIdle: {
     5        if (!dispense_request_future_.is_pendable()) {
     6          dispense_request_future_ = dispense_requests_.Receive();
     7        }
     8        PW_TRY_READY_ASSIGN(item_to_dispense_,
     9                            dispense_request_future_.Pend(cx));
    10        if (!item_to_dispense_.has_value()) {
    11          PW_LOG_WARN("Dispense requests channel unexpectedly closed.");
    12          return pw::async2::Ready();
    13        }
    14        SetDispenserMotorState(*item_to_dispense_, MotorState::kOn);
    15        state_ = kDispensing;
    16        break;
    17      }
    18
    19      case kDispensing: {
    20        if (!drop_future_.is_pendable()) {
    21          drop_future_ =
    22              pw::async2::Timeout(drop_sensor_.Wait(), kDispenseTimeout);
    23        }
    24        PW_TRY_READY_ASSIGN(auto result, drop_future_.Pend(cx));
    25        SetDispenserMotorState(*item_to_dispense_, MotorState::kOff);
    26        item_to_dispense_ = std::nullopt;
    27        if (result.ok()) {
    28          state_ = kReportDispenseSuccess;
    29        } else {
    30          state_ = kReportDispenseFailure;
    31        }
    32        break;
    33      }
    34
    35      case kReportDispenseSuccess: {
    36        if (!dispense_response_future_.is_pendable()) {
    37          dispense_response_future_ = dispense_responses_.Send(true);
    38        }
    39        PW_TRY_READY_ASSIGN(bool result, dispense_response_future_.Pend(cx));
    40        if (!result) {
    41          PW_LOG_WARN("Dispense responses channel unexpectedly closed.");
    42          return pw::async2::Ready();
    43        }
    44        state_ = kIdle;
    45        break;
    46      }
    47
    48      case kReportDispenseFailure: {
    49        if (!dispense_response_future_.is_pendable()) {
    50          dispense_response_future_ = dispense_responses_.Send(false);
    51        }
    52        PW_TRY_READY_ASSIGN(bool result, dispense_response_future_.Pend(cx));
    53        if (!result) {
    54          PW_LOG_WARN("Dispense responses channel unexpectedly closed.");
    55          return pw::async2::Ready();
    56        }
    57        state_ = kIdle;
    58        break;
    59      }
    60    }
    61  }
    62
    63  return pw::async2::Ready();
    64}
    65
    66}  // 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/basic_dispatcher.h"
#include "vending_machine.h"

namespace {

codelab::CoinSlot coin_slot;
codelab::Keypad keypad;
codelab::ItemDropSensor item_drop_sensor;

}  // 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::BasicDispatcher dispatcher;
  codelab::HardwareInit(&dispatcher);

  pw::async2::ChannelStorage<int, 1> dispense_requests;
  pw::async2::ChannelStorage<bool, 1> dispense_responses;

  auto [req_handle, dispense_requests_sender, dispense_requests_receiver] =
      pw::async2::CreateSpscChannel(dispense_requests);
  auto [res_handle, dispense_responses_sender, dispense_responses_receiver] =
      pw::async2::CreateSpscChannel(dispense_responses);
  req_handle.Release();
  res_handle.Release();

  codelab::VendingMachineTask task(coin_slot,
                                   keypad,
                                   std::move(dispense_requests_sender),
                                   std::move(dispense_responses_receiver));
  dispatcher.Post(task);

  codelab::DispenserTask dispenser_task(item_drop_sensor,
                                        std::move(dispense_requests_receiver),
                                        std::move(dispense_responses_sender));
  dispatcher.Post(dispenser_task);

  dispatcher.RunToCompletion();

  return 0;
}
#include "vending_machine.h"

#include <mutex>

#include "hardware.h"
#include "pw_async2/future_timeout.h"
#include "pw_async2/try.h"
#include "pw_log/log.h"
#include "pw_result/result.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);
          state_ = kAwaitingDispenseIdle;
          break;
        }

        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;
      }

      case kAwaitingDispenseIdle: {
        if (!dispense_request_future_.is_pendable()) {
          dispense_request_future_ = dispense_requests_.Send(item_to_dispense_);
        }
        PW_TRY_READY_ASSIGN(bool sent, dispense_request_future_.Pend(cx));
        if (!sent) {
          PW_LOG_WARN("Dispense requests channel unexpectedly closed.");
          return pw::async2::Ready();
        }
        state_ = kAwaitingDispense;
        break;
      }

      case kAwaitingDispense: {
        if (!dispense_response_future_.is_pendable()) {
          dispense_response_future_ = dispense_responses_.Receive();
        }
        std::optional<bool> received;
        PW_TRY_READY_ASSIGN(received, dispense_response_future_.Pend(cx));
        if (!received.has_value()) {
          PW_LOG_WARN("Dispense responses channel unexpectedly closed.");
          return pw::async2::Ready();
        }

        if (received.value()) {
          PW_LOG_INFO("Item dispensed successfully.");
          state_ = kWelcome;
        } else {
          PW_LOG_ERROR("Dispense failed. Choose another selection.");
          state_ = kAwaitingSelection;
        }
      }
    }
  }
}

pw::async2::Poll<> DispenserTask::DoPend(pw::async2::Context& cx) {
  while (true) {
    switch (state_) {
      case kIdle: {
        if (!dispense_request_future_.is_pendable()) {
          dispense_request_future_ = dispense_requests_.Receive();
        }
        PW_TRY_READY_ASSIGN(item_to_dispense_,
                            dispense_request_future_.Pend(cx));
        if (!item_to_dispense_.has_value()) {
          PW_LOG_WARN("Dispense requests channel unexpectedly closed.");
          return pw::async2::Ready();
        }
        SetDispenserMotorState(*item_to_dispense_, MotorState::kOn);
        state_ = kDispensing;
        break;
      }

      case kDispensing: {
        if (!drop_future_.is_pendable()) {
          drop_future_ =
              pw::async2::Timeout(drop_sensor_.Wait(), kDispenseTimeout);
        }
        PW_TRY_READY_ASSIGN(auto result, drop_future_.Pend(cx));
        SetDispenserMotorState(*item_to_dispense_, MotorState::kOff);
        item_to_dispense_ = std::nullopt;
        if (result.ok()) {
          state_ = kReportDispenseSuccess;
        } else {
          state_ = kReportDispenseFailure;
        }
        break;
      }

      case kReportDispenseSuccess: {
        if (!dispense_response_future_.is_pendable()) {
          dispense_response_future_ = dispense_responses_.Send(true);
        }
        PW_TRY_READY_ASSIGN(bool result, dispense_response_future_.Pend(cx));
        if (!result) {
          PW_LOG_WARN("Dispense responses channel unexpectedly closed.");
          return pw::async2::Ready();
        }
        state_ = kIdle;
        break;
      }

      case kReportDispenseFailure: {
        if (!dispense_response_future_.is_pendable()) {
          dispense_response_future_ = dispense_responses_.Send(false);
        }
        PW_TRY_READY_ASSIGN(bool result, dispense_response_future_.Pend(cx));
        if (!result) {
          PW_LOG_WARN("Dispense responses channel unexpectedly closed.");
          return pw::async2::Ready();
        }
        state_ = kIdle;
        break;
      }
    }
  }

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

}  // namespace codelab
#pragma once

#include <optional>
#include <utility>

#include "coin_slot.h"
#include "item_drop_sensor.h"
#include "pw_async2/channel.h"
#include "pw_async2/context.h"
#include "pw_async2/future.h"
#include "pw_async2/future_timeout.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::Sender<int> dispense_requests,
                     pw::async2::Receiver<bool> dispense_responses)
      : pw::async2::Task(PW_ASYNC_TASK_NAME("VendingMachineTask")),
        coin_slot_(coin_slot),
        keypad_(keypad),
        dispense_requests_(std::move(dispense_requests)),
        dispense_responses_(std::move(dispense_responses)),
        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_;
  pw::async2::Sender<int> dispense_requests_;
  pw::async2::Receiver<bool> dispense_responses_;
  pw::async2::SendFuture<int> dispense_request_future_;
  pw::async2::ReceiveFuture<bool> dispense_response_future_;
  unsigned coins_inserted_;
  int item_to_dispense_;

  enum State {
    kWelcome,
    kAwaitingPayment,
    kAwaitingSelection,
    kAwaitingDispenseIdle,
    kAwaitingDispense,
  };
  State state_;
};

class DispenserTask : public pw::async2::Task {
 public:
  DispenserTask(ItemDropSensor& drop_sensor,
                pw::async2::Receiver<int> dispense_requests,
                pw::async2::Sender<bool> dispense_responses)
      : pw::async2::Task(PW_ASYNC_TASK_NAME("DispenserTask")),
        drop_sensor_(drop_sensor),
        dispense_requests_(std::move(dispense_requests)),
        dispense_responses_(std::move(dispense_responses)),
        state_(kIdle) {}

 private:
  enum State {
    kIdle,
    kDispensing,
    kReportDispenseSuccess,
    kReportDispenseFailure,
  };

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

  ItemDropSensor& drop_sensor_;
  pw::async2::Receiver<int> dispense_requests_;
  pw::async2::Sender<bool> dispense_responses_;
  pw::async2::ReceiveFuture<int> dispense_request_future_;
  pw::async2::SendFuture<bool> dispense_response_future_;
  pw::async2::ValueFutureWithTimeout<void> drop_future_;
  std::optional<int> item_to_dispense_;
  State state_;

  static constexpr auto kDispenseTimeout = std::chrono::seconds(5);
};

}  // namespace codelab