3. Create and wake a Future#
pw_async2: Cooperative async tasks for embedded
Currently, your vending machine immediately dispenses an item after receiving
coins. In this step, you modify your app to let customers choose what to buy.
Along the way you will learn how to correctly wake up a task that is waiting for
input like this. You will also gain some experience implementing a Future
yourself.
In practice, you would not write a custom Future for such a common case as
this. Pigweed provides ValueFuture for the purpose of returning a single
async value. At the end of this step, we will use FutureCore to simplify
our custom Future implementation.
Stub the Keypad class#
The hardware simulator sends you keypad events via async calls to
key_press_isr(). The simulator sends integer values between 0 and 9
that indicate which keypad button was pressed. Your mission is to process
these keypad events safely and to allow your task to wait for keypad
numbers after receiving coins.
Use the keypad in
main.cc:Declare a global
keypadinstanceUpdate
key_press_isr()to handle keypad input events by invokingkeypad.Press(key)Pass a reference to the keypad when creating your task instance
Hint
1namespace { 2 3codelab::CoinSlot coin_slot; 4codelab::Keypad keypad; 5 6} // namespace 7 8// Interrupt handler function invoked when the user inserts a coin into the 9// vending machine. 10void coin_inserted_isr() { coin_slot.Deposit(); } 11 12// Interrupt handler function invoked when the user presses a key on the 13// machine's keypad. Receives the value of the pressed key (0-9). 14void key_press_isr(int key) { keypad.Press(key); } 15 16// Interrupt handler function invoked to simulate the item drop detector 17// detecting confirmation that an item was successfully dispensed from the 18// machine. 19void item_drop_sensor_isr() { 20 // In Step 5 you will uses this as part of a new Dispense task that runs 21 // the dispenser motor until an item drops, or you time out on the vend 22 // operation. 23} 24 25int main() { 26 pw::async2::BasicDispatcher dispatcher; 27 codelab::HardwareInit(&dispatcher); 28 29 codelab::VendingMachineTask task(coin_slot, keypad); 30 dispatcher.Post(task); 31 32 dispatcher.RunToCompletion(); 33 34 return 0; 35}
Set up the
KeypadandKeyPressFutureclasses invending_machine.h:Declare the stub
KeypadandKeyPressFutureclasses:1class Keypad { 2 public: 3 // Returns a future that resolves when a key is pressed with the value 4 // of the key. 5 // 6 // May only be called by one task. 7 KeyPressFuture WaitForKeyPress(); 8 9 // Record a key press. Typically called from the keypad ISR. 10 void Press(int key); 11 12 private: 13 // The future that will be resolved when a key is pressed. 14 KeyPressFuture* key_press_future_ = nullptr; 15}; 16 17class KeyPressFuture { 18 public: 19 // The type returned by the future when it completes. 20 using value_type = int; 21 22 KeyPressFuture() : state_(kDefaultConstructed) {} 23 24 // Pends until a key is pressed, returning the key number. 25 pw::async2::Poll<value_type> Pend(pw::async2::Context& cx); 26 27 bool is_pendable() const { return state_ == kPendable; } 28 bool is_complete() const { return state_ == kCompleted; } 29 30 private: 31 friend class Keypad; 32 33 // Stub constructor that initializes the future with a key press. 34 // We will replace this later. 35 explicit KeyPressFuture(int key_pressed) 36 : state_(kPendable), key_pressed_(key_pressed) {} 37 38 // Possible states of the future. 39 enum { 40 kDefaultConstructed, 41 kPendable, 42 kCompleted, 43 } state_; 44 45 // When present, holds the key that was pressed. 46 // If absent, the future is still pending. 47 std::optional<int> key_pressed_; 48}; 49 50// Ensure that KeyPressFuture satisfies the Future concept. 51static_assert(pw::async2::Future<KeyPressFuture>);
Futures are required to expose four members:
value_type: The type returned by the future when it completesPend(Context& cx): Pend until the future is readyis_pendable(): Return true if the future is valid and has not yet completed (i.e. handed out by some async operation rather than default constructed)is_complete(): Return true if the future completed and its value has been consumed
The concept
pw_async2::Futurecan be used to ensure that your type conforms to theFutureinterface.Set up a private
keypad_andkey_future_member forVendingMachineTaskAdd a
Keypad&argument to theVendingMachineTaskconstructor parameters and use it to initialize thekeypad_memberSet up an
unsigned coins_inserted_data member inVendingMachineTaskthat tracks how many coins have been inserted and initializecoins_inserted_to0in the constructor
Hint
1namespace codelab { 2 3class KeyPressFuture; 4 5class Keypad { 6 public: 7 // Returns a future that resolves when a key is pressed with the value 8 // of the key. 9 // 10 // May only be called by one task. 11 KeyPressFuture WaitForKeyPress(); 12 13 // Record a key press. Typically called from the keypad ISR. 14 void Press(int key); 15 16 private: 17 // The future that will be resolved when a key is pressed. 18 KeyPressFuture* key_press_future_ = nullptr; 19}; 20 21class KeyPressFuture { 22 public: 23 // The type returned by the future when it completes. 24 using value_type = int; 25 26 KeyPressFuture() : state_(kDefaultConstructed) {} 27 28 // Pends until a key is pressed, returning the key number. 29 pw::async2::Poll<value_type> Pend(pw::async2::Context& cx); 30 31 bool is_pendable() const { return state_ == kPendable; } 32 bool is_complete() const { return state_ == kCompleted; } 33 34 private: 35 friend class Keypad; 36 37 // Stub constructor that initializes the future with a key press. 38 // We will replace this later. 39 explicit KeyPressFuture(int key_pressed) 40 : state_(kPendable), key_pressed_(key_pressed) {} 41 42 // Possible states of the future. 43 enum { 44 kDefaultConstructed, 45 kPendable, 46 kCompleted, 47 } state_; 48 49 // When present, holds the key that was pressed. 50 // If absent, the future is still pending. 51 std::optional<int> key_pressed_; 52}; 53 54// Ensure that KeyPressFuture satisfies the Future concept. 55static_assert(pw::async2::Future<KeyPressFuture>); 56 57// The main task that drives the vending machine. 58class VendingMachineTask : public pw::async2::Task { 59 public: 60 VendingMachineTask(CoinSlot& coin_slot, Keypad& keypad) 61 : pw::async2::Task(PW_ASYNC_TASK_NAME("VendingMachineTask")), 62 coin_slot_(coin_slot), 63 keypad_(keypad), 64 coins_inserted_(0) {} 65 66 private: 67 // This is the core of the asynchronous task. The dispatcher calls this method 68 // to give the task a chance to do work. 69 pw::async2::Poll<> DoPend(pw::async2::Context& cx) override; 70 71 CoinSlot& coin_slot_; 72 CoinFuture coin_future_; 73 Keypad& keypad_; 74 KeyPressFuture key_future_; 75 unsigned coins_inserted_; 76}; 77 78} // namespace codelab
Create a stub implementation and integrate it (as well as the coin count data member) in
vending_machine.cc:Create a stub
KeyPressFutureimplementation:1pw::async2::Poll<int> KeyPressFuture::Pend(pw::async2::Context& cx) { 2 if (key_pressed_.has_value()) { 3 return pw::async2::Ready(key_pressed_.value()); 4 } 5 return pw::async2::Pending(); 6} 7 8KeyPressFuture Keypad::WaitForKeyPress() { return KeyPressFuture(-1); } 9 10void Keypad::Press(int key) {}
In the
Pendmember function we return the value of the key if it exists, orPendingotherwise.WaitForKeyPress()always returns a future its value set to-1, so it completes immediately. We will fix that later.Update
VendingMachineTask::DoPend()to check if coins have already been inserted before it pends for coins (coin_future_.Pend(cx))Obtain a
KeyPressFutureand wait for keypad input by invokingkey_future_.Pend(cx)after coins have been inserted
Hint
1pw::async2::Poll<int> KeyPressFuture::Pend(pw::async2::Context& cx) { 2 if (key_pressed_.has_value()) { 3 return pw::async2::Ready(key_pressed_.value()); 4 } 5 return pw::async2::Pending(); 6} 7 8KeyPressFuture Keypad::WaitForKeyPress() { return KeyPressFuture(-1); } 9 10void Keypad::Press(int key) {} 11 12pw::async2::Poll<> VendingMachineTask::DoPend(pw::async2::Context& cx) { 13 if (coins_inserted_ == 0) { 14 if (!coin_future_.is_pendable()) { 15 PW_LOG_INFO("Welcome to the Pigweed Vending Machine!"); 16 PW_LOG_INFO("Please insert a coin."); 17 coin_future_ = coin_slot_.GetCoins(); 18 } 19 20 PW_TRY_READY_ASSIGN(unsigned coins, coin_future_.Pend(cx)); 21 PW_LOG_INFO("Received %u coin%s.", coins, coins > 1 ? "s" : ""); 22 PW_LOG_INFO("Please press a keypad key."); 23 coins_inserted_ += coins; 24 } 25 26 if (!key_future_.is_pendable()) { 27 key_future_ = keypad_.WaitForKeyPress(); 28 } 29 30 PW_TRY_READY_ASSIGN(int key, key_future_.Pend(cx)); 31 PW_LOG_INFO("Keypad %d was pressed. Dispensing an item.", key); 32 33 return pw::async2::Ready(); 34}
Verify the stub implementation#
Run the app:
bazelisk run //pw_async2/codelabPress c Enter to insert a coin.
You should see a log stating that
-1was pressed. This is expected since theKeyPad::Pend()stub implementation returnskey_pressed_, which was initialized tokNone(-1).INF Welcome to the Pigweed Vending Machine! INF Please insert a coin. c INF Received 1 coin. INF Please press a keypad key. INF Keypad -1 was pressed. Dispensing an item.
Handle keypad events#
Now, let’s update our Keypad and KeyPressFuture implementations to
actually handle key presses.
Track the active
KeyPressFutureinKeypad:In order to resolve a future when a key is pressed, they
Keypadneeds to be aware of the active future. However, as the future is owned by the caller, they can freely move it around. Therefore, we also needKeyPressFutureto keep a reference to itsKeypadto update its pointer if moved.The full code for these operations is given below:
1KeyPressFuture::KeyPressFuture(KeyPressFuture&& other) noexcept 2 : state_(std::exchange(other.state_, kDefaultConstructed)), 3 keypad_(std::exchange(other.keypad_, nullptr)), 4 key_pressed_(std::exchange(other.key_pressed_, std::nullopt)) { 5 std::lock_guard lock(internal::KeypadLock()); 6 if (keypad_ != nullptr) { 7 if (keypad_->key_press_future_ == &other) { 8 keypad_->key_press_future_ = this; 9 } 10 } 11} 12 13KeyPressFuture& KeyPressFuture::operator=(KeyPressFuture&& other) noexcept { 14 if (this != &other) { 15 state_ = std::exchange(other.state_, kDefaultConstructed); 16 keypad_ = std::exchange(other.keypad_, nullptr); 17 key_pressed_ = std::exchange(other.key_pressed_, std::nullopt); 18 19 std::lock_guard lock(internal::KeypadLock()); 20 if (keypad_ != nullptr) { 21 if (keypad_->key_press_future_ == &other) { 22 keypad_->key_press_future_ = this; 23 } else if (keypad_->key_press_future_ == this) { 24 keypad_->key_press_future_ = nullptr; 25 } 26 } 27 } 28 return *this; 29}
Note
You should rarely have to write these boilerplate move operations yourself as Pigweed provides utilities to handle this for you, which will be introduced in a later step.
Add a
Keypad*pointer toKeyPressFutureand replace the stub value constructor with a constructor that accepts aKeypad*, setting the state tokInitializedand the new future as the keypad’s active future.
Hint
1 explicit KeyPressFuture(Keypad& keypad) 2 : state_(kInitialized), keypad_(&keypad) { 3 std::lock_guard lock(internal::KeypadLock()); 4 keypad_->key_press_future_ = this; 5 }
Update
WaitForKeyPress()to passthisto theKeyPressFutureconstructor.
Protect keypad data access in
vending_machine.h:Include the
pw_sync/interrupt_spin_lock.handpw_sync/lock_annotations.hheadersDefine an internal
KeypadLockfunction to return a reference to a staticpw::sync::InterruptSpinLock.Note
We use a global lock here to safely handle destructor ordering issues. This logic mimics pw::async2::ValueFuture, which uses a similar pattern. While we are building this manually for learning purposes, in real code you would just use
ValueFuturedirectly, which handles this for you.Guard
key_press_future_with the spin lock with PW_GUARDED_BY
Since the keypad ISR is asynchronous, you’ll need to synchronize access to the stored event data. For this codelab, we use pw::sync::InterruptSpinLock which is safe to acquire from an ISR in production use. Alternatively you can use atomic operations.
We use PW_GUARDED_BY to add a compile-time check to ensure that the protected key future is only accessed when the spin lock is held.
Hint
1#include <optional> 2 3#include "coin_slot.h" 4#include "pw_async2/context.h" 5#include "pw_async2/future.h" 6#include "pw_async2/poll.h" 7#include "pw_async2/task.h" 8#include "pw_sync/interrupt_spin_lock.h" 9#include "pw_sync/lock_annotations.h" 10 11namespace codelab { 12namespace internal { 13 14inline pw::sync::InterruptSpinLock& KeypadLock() { 15 static pw::sync::InterruptSpinLock lock; 16 return lock; 17} 18 19} // namespace internal 20 21class KeyPressFuture; 22 23class Keypad { 24 public: 25 // Returns a future that resolves when a key is pressed with the value 26 // of the key. 27 // 28 // May only be called by one task. 29 KeyPressFuture WaitForKeyPress(); 30 31 // Record a key press. Typically called from the keypad ISR. 32 void Press(int key); 33 34 private: 35 friend class KeyPressFuture; 36 37 // The future that will be resolved when a key is pressed. 38 KeyPressFuture* key_press_future_ PW_GUARDED_BY(internal::KeypadLock()) = 39 nullptr; 40};
Implement keypress handling in
vending_machine.cc:In
Keypad::Press(), store the keypress data in the active future if one exists. Remember to hold the lock!
Hint
1pw::async2::Poll<int> KeyPressFuture::Pend(pw::async2::Context& cx) { 2 if (key_pressed_.has_value()) { 3 return pw::async2::Ready(key_pressed_.value()); 4 } 5 return pw::async2::Pending(); 6} 7 8KeyPressFuture Keypad::WaitForKeyPress() { return KeyPressFuture(*this); } 9 10void Keypad::Press(int key) { 11 std::lock_guard lock(internal::KeypadLock()); 12 if (key_press_future_ != nullptr) { 13 key_press_future_->key_pressed_ = key; 14 } 15}
And we’re done… what could go wrong?
Test the keypad implementation#
Run the app:
bazelisk run //pw_async2/codelabPress c Enter to insert a coin.
INF Welcome to the Pigweed Vending Machine! INF Please insert a coin. c INF Received 1 coin. INF Please press a keypad key. ▄████▄ ██▀███ ▄▄▄ ██████ ██░ ██ ▒██▀ ▀█ ▓██ ▒ ██▒ ▒████▄ ▒██ ▒ ▓██░ ██▒ ▒▓█ 💥 ▄ ▓██ ░▄█ ▒ ▒██ ▀█▄ ░ ▓██▄ ▒██▀▀██░ ▒▓▓▄ ▄██▒ ▒██▀▀█▄ ░██▄▄▄▄██ ▒ ██▒ ░▓█ ░██ ▒ ▓███▀ ░ ░██▓ ▒██▒ ▓█ ▓██▒ ▒██████▒▒ ░▓█▒░██▓ ░ ░▒ ▒ ░ ░ ▒▓ ░▒▓░ ▒▒ ▓▒█░ ▒ ▒▓▒ ▒ ░ ▒ ░░▒░▒ ░ ▒ ░▒ ░ ▒░ ▒ ▒▒ ░ ░ ░▒ ░ ░ ▒ ░▒░ ░ ░ ░░ ░ ░ ▒ ░ ░ ░ ░ ░░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ pw_async2/task.cc:186: PW_CHECK() or PW_DCHECK() FAILED! FAILED ASSERTION !wakers_.empty() FILE & LINE pw_async2/task.cc:186 FUNCTION Task::RunResult pw::async2::Task::RunInDispatcher() MESSAGE Task VendingMachineTask:0x16b48e5b0 returned Pending() without registering a wakerTo prevent obviously incorrect usage, the
pw_async2module asserts if you returnPending()without actually storing aWaker, because it means your task has no way of being woken back up.
Store a waker#
In general, you should always store a Waker before
returning Pending(). A waker is a lightweight object that allows you to
tell the dispatcher to wake a task. When a Task::DoPend() call returns
Pending(), the task is put to sleep so that the dispatcher doesn’t have to
repeatedly poll the task. Without a waker, the task will sleep forever.
Declare a waker in
vending_machine.h:Include the
pw_async2/waker.hheaderAdd a
pw::async2::Waker waker_private member to theKeyPressFutureclass
Hint
1#include <optional> 2 3#include "coin_slot.h" 4#include "pw_async2/context.h" 5#include "pw_async2/future.h" 6#include "pw_async2/poll.h" 7#include "pw_async2/task.h" 8#include "pw_async2/waker.h" 9#include "pw_sync/interrupt_spin_lock.h" 10#include "pw_sync/lock_annotations.h" 11 12namespace codelab { 13namespace internal { 14 15inline pw::sync::InterruptSpinLock& KeypadLock() { 16 static pw::sync::InterruptSpinLock lock; 17 return lock; 18} 19 20} // namespace internal 21 22class KeyPressFuture; 23 24class Keypad { 25 public: 26 // Returns a future that resolves when a key is pressed with the value 27 // of the key. 28 // 29 // May only be called by one task. 30 KeyPressFuture WaitForKeyPress(); 31 32 // Record a key press. Typically called from the keypad ISR. 33 void Press(int key); 34 35 private: 36 friend class KeyPressFuture; 37 38 // The future that will be resolved when a key is pressed. 39 KeyPressFuture* key_press_future_ PW_GUARDED_BY(internal::KeypadLock()) = 40 nullptr; 41}; 42 43class KeyPressFuture { 44 public: 45 // The type returned by the future when it completes. 46 using value_type = int; 47 48 KeyPressFuture() : state_(kDefaultConstructed) {} 49 50 KeyPressFuture(const KeyPressFuture&) = delete; 51 KeyPressFuture& operator=(const KeyPressFuture&) = delete; 52 53 KeyPressFuture(KeyPressFuture&& other) noexcept; 54 KeyPressFuture& operator=(KeyPressFuture&& other) noexcept; 55 56 // Pends until a key is pressed, returning the key number. 57 pw::async2::Poll<value_type> Pend(pw::async2::Context& cx); 58 59 bool is_pendable() const { return state_ == kInitialized; } 60 bool is_complete() const { return state_ == kCompleted; } 61 62 private: 63 friend class Keypad; 64 65 explicit KeyPressFuture(Keypad& keypad) 66 : state_(kInitialized), keypad_(&keypad) { 67 std::lock_guard lock(internal::KeypadLock()); 68 keypad_->key_press_future_ = this; 69 } 70 71 // Possible states of the future. 72 enum { 73 kDefaultConstructed, 74 kInitialized, 75 kCompleted, 76 } state_; 77 78 Keypad* keypad_; 79 80 // When present, holds the key that was pressed. 81 // If absent, the future is still pending. 82 std::optional<int> key_pressed_; 83 84 pw::async2::Waker waker_; 85}; 86 87// Ensure that KeyPressFuture satisfies the Future concept. 88static_assert(pw::async2::Future<KeyPressFuture>);
Use the waker in
vending_machine.cc:In
KeyPressFuture::Pend()store the waker before returningPending():PW_ASYNC_STORE_WAKER(cx, waker_, "Waiting for keypad press");
The last argument should always be a meaningful string describing the wait reason. In the next section you’ll see how this string can help you debug issues.
Hint
1pw::async2::Poll<int> KeyPressFuture::Pend(pw::async2::Context& cx) { 2 if (key_pressed_.has_value()) { 3 return pw::async2::Ready(key_pressed_.value()); 4 } 5 PW_ASYNC_STORE_WAKER(cx, waker_, "Waiting for keypad press"); 6 return pw::async2::Pending(); 7} 8 9KeyPressFuture Keypad::WaitForKeyPress() { return KeyPressFuture(*this); } 10 11void Keypad::Press(int key) { 12 std::lock_guard lock(internal::KeypadLock()); 13 if (key_press_future_ != nullptr) { 14 key_press_future_->key_pressed_ = key; 15 } 16}
Tip
See Setting up wakers for an overview of all of the different ways that you can set up wakers.
Forget to wake the task#
You’ve set up the waker but you’re not using it yet. Let’s see what happens.
Run the app:
bazelisk run //pw_async2/codelabPress c Enter to insert a coin.
Press 1 Enter to select an item.
You should see output like this:
INF Welcome to the Pigweed Vending Machine! INF Please insert a coin. c INF Received 1 coin. INF Please press a keypad key. 1
Nothing happens, not even an assertion! This time there is no crash! Why??
Answer
The problem is that
pw_async2has no way of knowing when the task is ready to be woken up.The reason
pw_async2can detect forgetting to store the waker, on the other hand, is because it happens during apw_async2-initiated call into your code, so there can be a postcondition check.Debug this issue by pressing d Enter:
You should see output like this:
d INF pw::async2::Dispatcher INF Woken tasks: INF Sleeping tasks: INF - VendingMachineTask:0x7ffeec48fd90 (1 wakers) INF * Waker 1: Waiting for keypad press
This shows the state of all the tasks registered with the dispatcher. The last line is the wait reason string that you provided when you registered the waker. We can see that the vending machine task is still sleeping.
Tip
If you use
pw_async2in your own project, you can get this kind of debug information by calling LogRegisteredTasks.If you don’t see the reason messages, make sure that PW_ASYNC2_DEBUG_WAIT_REASON is not set to
0.
Wake the task#
Fix the issue in
vending_machine.cc:When the keypad is pressed, invoke the Wake() method on the
Waker:1void Keypad::Press(int key) { 2 std::lock_guard lock(internal::KeypadLock()); 3 if (key_press_future_ != nullptr) { 4 key_press_future_->key_pressed_ = key; 5 key_press_future_->waker_.Wake(); 6 }
By design, the
Wake()call consumes theWaker, leaving it in an empty state. Additionally, wakers are default-constructed in an empty state, and moving the value means the location that is moved from is reset to an empty state. If you invokeWake()on an emptyWaker, the call is a no-op.
Verify the fix:
bazelisk run //pw_async2/codelabPress c Enter to insert a coin.
Press 1 Enter to select an item.
You should see keypad input working correctly now:
INF Welcome to the Pigweed Vending Machine! INF Please insert a coin. c INF Received 1 coin. INF Please press a keypad key. 1 INF Keypad 1 was pressed. Dispensing an item.
Tip
You can also end up in a “task not waking up” state if you destroy or
otherwise clear the Waker instance that pointed at the task to wake.
LogRegisteredTasks() can also help here by pointing to a problem related
to waking your task.
Simplify with FutureCore#
The manual implementation of KeyPressFuture is complex and error-prone due
to the need to manage move semantics and pointers between the future and the
keypad, as well as having to manage a Waker.
Pigweed provides pw::async2::FutureCore to handle these details for you.
FutureCore manages the listing of futures in a linked list owned by a future
provider like the Keypad and automatically stores a Waker when returning
Pending.
Update
vending_machine.hto useFutureCore:Include
pw_async2/future.h.Replace the
waker_member in your customKeyPressFuturewith apw::async2::FutureCore core_, then delete your custom move constructor and move assignment operator. The default move operations provided by the compiler will handle everything for you.Make
pw::async2::FutureCorea friend ofKeyPressFuture.Replace the
key_press_future_pointer inKeypadwith apw::async2::FutureList<&KeyPressFuture::core_> futures_. By templating it on a member pointer to a concrete future’sFutureCore, theFutureListcan retrieve theKeyPressFutureitself.To set the waker’s wait reason, define a
static constexpr const char kWaitReason[]member inKeyPressFuturewith the wait reason string.
Hint
1class KeyPressFuture { 2 public: 3 // The type returned by the future when it completes. 4 using value_type = int; 5 6 KeyPressFuture() : core_(), keypad_(nullptr) {} 7 8 // Pends until a key is pressed, returning the key number. 9 pw::async2::Poll<value_type> Pend(pw::async2::Context& cx); 10 11 [[nodiscard]] bool is_pendable() const { return core_.is_pendable(); } 12 [[nodiscard]] bool is_complete() const { return core_.is_complete(); } 13 14 private: 15 friend class Keypad; 16 friend class pw::async2::FutureCore; 17 18 static constexpr const char kWaitReason[] = "Waiting for keypad press"; 19 20 explicit KeyPressFuture(Keypad& keypad); 21 22 pw::async2::Poll<value_type> DoPend(pw::async2::Context& cx); 23 24 pw::async2::FutureCore core_; 25 Keypad* keypad_; 26 27 // When present, holds the key that was pressed. 28 // If absent, the future is still pending. 29 std::optional<int> key_pressed_ PW_GUARDED_BY(internal::KeypadLock()); 30}; 31 32// Ensure that KeyPressFuture satisfies the Future concept. 33static_assert(pw::async2::Future<KeyPressFuture>); 34 35class Keypad { 36 public: 37 // Returns a future that resolves when a key is pressed with the value 38 // of the key. 39 // 40 // May only be called by one task. 41 KeyPressFuture WaitForKeyPress(); 42 43 // Record a key press. Typically called from the keypad ISR. 44 void Press(int key); 45 46 private: 47 friend class KeyPressFuture; 48 49 // The list of futures waiting for a key press. 50 pw::async2::FutureList<&KeyPressFuture::core_> futures_ 51 PW_GUARDED_BY(internal::KeypadLock()); 52};
Update
vending_machine.ccto implement the simplified logic:Delete all the manual move logic.
In the
KeyPressFutureconstructor, initialize the list and push the future to the keypad’sfutures_list.In
Keypad::Press, usefutures_.PopIfAvailable()to retrieve the active future, then set itskey_pressed_member and wake it.Rename your
KeyPressFuture::PendtoDoPend, then add a simplePendmethod that delegates tocore_.DoPend(*this, cx).
Hint
1KeyPressFuture::KeyPressFuture(Keypad& keypad) 2 : core_(pw::async2::FutureState::kPending), keypad_(&keypad) { 3 std::lock_guard lock(internal::KeypadLock()); 4 keypad_->futures_.Push(*this); 5} 6 7pw::async2::Poll<int> KeyPressFuture::Pend(pw::async2::Context& cx) { 8 return core_.DoPend(*this, cx); 9} 10 11pw::async2::Poll<int> KeyPressFuture::DoPend(pw::async2::Context&) { 12 std::lock_guard lock(internal::KeypadLock()); 13 if (key_pressed_.has_value()) { 14 return pw::async2::Ready(*key_pressed_); 15 } 16 return pw::async2::Pending(); 17} 18 19KeyPressFuture Keypad::WaitForKeyPress() { return KeyPressFuture(*this); } 20 21void Keypad::Press(int key) { 22 std::lock_guard lock(internal::KeypadLock()); 23 if (auto future = futures_.PopIfAvailable()) { 24 future->key_pressed_ = key; 25 future->core_.Wake(); 26 } 27}
Next steps#
Continue to 4. Use a state machine to learn how to manage the rapidly increasing complexity of your code.
Checkpoint#
At this point, your code should look similar to the files below.
#include "coin_slot.h"
#include "hardware.h"
#include "pw_async2/basic_dispatcher.h"
#include "vending_machine.h"
namespace {
codelab::CoinSlot coin_slot;
codelab::Keypad keypad;
} // namespace
// Interrupt handler function invoked when the user inserts a coin into the
// vending machine.
void coin_inserted_isr() { coin_slot.Deposit(); }
// Interrupt handler function invoked when the user presses a key on the
// machine's keypad. Receives the value of the pressed key (0-9).
void key_press_isr(int key) { keypad.Press(key); }
// Interrupt handler function invoked to simulate the item drop detector
// detecting confirmation that an item was successfully dispensed from the
// machine.
void item_drop_sensor_isr() {
// In Step 5 you will uses this as part of a new Dispense task that runs
// the dispenser motor until an item drops, or you time out on the vend
// operation.
}
int main() {
pw::async2::BasicDispatcher dispatcher;
codelab::HardwareInit(&dispatcher);
codelab::VendingMachineTask task(coin_slot, keypad);
dispatcher.Post(task);
dispatcher.RunToCompletion();
return 0;
}
#include "vending_machine.h"
#include <mutex>
#include "pw_async2/try.h"
#include "pw_log/log.h"
namespace codelab {
KeyPressFuture::KeyPressFuture(Keypad& keypad)
: core_(pw::async2::FutureState::kPending), keypad_(&keypad) {
std::lock_guard lock(internal::KeypadLock());
keypad_->futures_.Push(*this);
}
pw::async2::Poll<int> KeyPressFuture::Pend(pw::async2::Context& cx) {
return core_.DoPend(*this, cx);
}
pw::async2::Poll<int> KeyPressFuture::DoPend(pw::async2::Context&) {
std::lock_guard lock(internal::KeypadLock());
if (key_pressed_.has_value()) {
return pw::async2::Ready(*key_pressed_);
}
return pw::async2::Pending();
}
KeyPressFuture Keypad::WaitForKeyPress() { return KeyPressFuture(*this); }
void Keypad::Press(int key) {
std::lock_guard lock(internal::KeypadLock());
if (auto future = futures_.PopIfAvailable()) {
future->key_pressed_ = key;
future->core_.Wake();
}
}
pw::async2::Poll<> VendingMachineTask::DoPend(pw::async2::Context& cx) {
if (coins_inserted_ == 0) {
if (!coin_future_.is_pendable()) {
PW_LOG_INFO("Welcome to the Pigweed Vending Machine!");
PW_LOG_INFO("Please insert a coin.");
coin_future_ = coin_slot_.GetCoins();
}
PW_TRY_READY_ASSIGN(unsigned coins, coin_future_.Pend(cx));
PW_LOG_INFO("Received %u coin%s.", coins, coins > 1 ? "s" : "");
PW_LOG_INFO("Please press a keypad key.");
coins_inserted_ += coins;
}
if (!key_future_.is_pendable()) {
key_future_ = keypad_.WaitForKeyPress();
}
PW_TRY_READY_ASSIGN(int key, key_future_.Pend(cx));
PW_LOG_INFO("Keypad %d was pressed. Dispensing an item.", key);
return pw::async2::Ready();
}
} // namespace codelab
#pragma once
#include <optional>
#include "coin_slot.h"
#include "pw_async2/context.h"
#include "pw_async2/future.h"
#include "pw_async2/poll.h"
#include "pw_async2/task.h"
#include "pw_sync/interrupt_spin_lock.h"
#include "pw_sync/lock_annotations.h"
namespace codelab {
namespace internal {
inline pw::sync::InterruptSpinLock& KeypadLock() {
static pw::sync::InterruptSpinLock lock;
return lock;
}
} // namespace internal
class Keypad;
class KeyPressFuture {
public:
// The type returned by the future when it completes.
using value_type = int;
KeyPressFuture() : core_(), keypad_(nullptr) {}
// Pends until a key is pressed, returning the key number.
pw::async2::Poll<value_type> Pend(pw::async2::Context& cx);
[[nodiscard]] bool is_pendable() const { return core_.is_pendable(); }
[[nodiscard]] bool is_complete() const { return core_.is_complete(); }
private:
friend class Keypad;
friend class pw::async2::FutureCore;
static constexpr const char kWaitReason[] = "Waiting for keypad press";
explicit KeyPressFuture(Keypad& keypad);
pw::async2::Poll<value_type> DoPend(pw::async2::Context& cx);
pw::async2::FutureCore core_;
Keypad* keypad_;
// When present, holds the key that was pressed.
// If absent, the future is still pending.
std::optional<int> key_pressed_ PW_GUARDED_BY(internal::KeypadLock());
};
// Ensure that KeyPressFuture satisfies the Future concept.
static_assert(pw::async2::Future<KeyPressFuture>);
class Keypad {
public:
// Returns a future that resolves when a key is pressed with the value
// of the key.
//
// May only be called by one task.
KeyPressFuture WaitForKeyPress();
// Record a key press. Typically called from the keypad ISR.
void Press(int key);
private:
friend class KeyPressFuture;
// The list of futures waiting for a key press.
pw::async2::FutureList<&KeyPressFuture::core_> futures_
PW_GUARDED_BY(internal::KeypadLock());
};
// The main task that drives the vending machine.
class VendingMachineTask : public pw::async2::Task {
public:
VendingMachineTask(CoinSlot& coin_slot, Keypad& keypad)
: pw::async2::Task(PW_ASYNC_TASK_NAME("VendingMachineTask")),
coin_slot_(coin_slot),
keypad_(keypad),
coins_inserted_(0) {}
private:
// This is the core of the asynchronous task. The dispatcher calls this method
// to give the task a chance to do work.
pw::async2::Poll<> DoPend(pw::async2::Context& cx) override;
CoinSlot& coin_slot_;
CoinFuture coin_future_;
Keypad& keypad_;
KeyPressFuture key_future_;
unsigned coins_inserted_;
};
} // namespace codelab