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.
Integrate the item drop sensor into
main.cc
:Include
item_drop_sensor.h
Create a global
codelab::ItemDropSensor item_drop_sensor
instanceCall 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.
Set up the queues in
vending_machine.h
:Include
pw_containers/inline_async_queue.h
Create a
DispenseRequestQueue
alias forpw::InlineAsyncQueue<int, 1>
and aDispenseResponseQueue
alias forpw::InlineAsyncQueue<bool, 1>
You’ll use
DispenseRequestQueue
to send dispense requests (item numbers) fromVendingMachineTask
toDispenserTask
.DispenseResponseQueue
is for sending dispense responses (success or failure) fromDispenserTask
toVendingMachineTask
.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>;
Back in
main.cc
, set up the queues:Declare a
dispense_requests
queue and adispense_response
queueProvide the queues to
VendingMachineTask
when the task is createdCreate 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
.
Declare a new
DispenserTask
class invending_machine.h
:The
DispenserTask
constructor should accept references to the drop sensor and both comms queues as argsCreate 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};
Implement the dispenser’s state machine in
vending_machine.cc
:Handle the
kIdle
,kDispensing
, andkReportDispenseSuccess
states (as well as the transitions between them)Use the
SetDispenserMotorState
function that’s provided inhardware.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.
Prepare
VendingMachineTask
for comms invending_machine.h
:Add the communication queues as parameters to the
VendingMachineTask
constructorAdd new states:
kAwaitingDispenseIdle
(dispenser is ready for a request) andkAwaitingDispense
(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};
Update the vending machine task’s state machine in
vending_machine.cc
:Transition the
kAwaitingSelection
state tokAwaitingDispenseIdle
Implement the
kAwaitingDispenseIdle
andkAwaitingDispense
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#
Run the app:
bazelisk run //pw_async2/codelab
Press c Enter to input a coin.
Press 1 Enter to make a selection.
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.
Prepare
DispenserTask
to support timeouts invending_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 newkDispenseTimeout
data member inDispenserTask
that holds the timeout duration (std::chrono::seconds(5)
is a good value), and apw::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.
Implement the timeout support in
vending_machine.cc
:When you start dispensing an item (in your transition from
kIdle
tokDispensing
), 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 wakeDispenserTask
unnecessarily, wasting time and power. After that, proceed to thekReportDispenseSuccess
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#
Run the app:
bazelisk run //pw_async2/codelab
Press c Enter to input a coin.
# Press 1 Enter to make a selection.
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.
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