blob: b5d9727dffefd5733ed242260bbe99c458bc210e [file] [edit]
.. _module-pw_async2-codelab-step5:
============================
5. Communicate between tasks
============================
.. pigweed-module-subpage::
:name: pw_async2
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`` 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.
.. dropdown:: Hint
.. literalinclude:: ./checkpoint1/main.cc
:language: cpp
:linenos:
:start-after: // the License.
:end-before: int main() {
:emphasize-lines: 3,11,26
-----------------------------
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.
#. 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.
.. dropdown:: Hint
.. literalinclude:: ./checkpoint1/vending_machine.h
:language: cpp
:linenos:
:lines: 1-24,27-38
:emphasize-lines: 5-6,10-11,23-24
:start-at: class VendingMachineTask
:end-before: class DispenserTask
#. 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
:ref:`Channel docs <module-pw_async2-channels>` 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
.. dropdown:: Hint
.. literalinclude:: ./checkpoint1/main.cc
:language: cpp
:linenos:
:start-at: int main() {
:emphasize-lines: 5-13,17-18,21-24
.. topic:: Lifecycle of an async2 Channel
Channels begin open when created through ``CreateSpscChannel``,
``CreateMpscChannel``, ``CreateSpmcChannel``, or ``CreateMpmcChannel``.
Each of these functions returns a handle to the channel, optionally alongside
a sender and/or a receiver depending on the channel type.
Senders and receivers are movable but not copyable. If a specific channel
type supports multiple senders or receivers, they are created by calling
``CreateSender()`` or ``CreateReceiver()`` on the handle.
In addition to creating senders and receivers, the channel handle has a
``Close()`` function which is used to close the channel early, bypassing its
natural lifecycle. Channel handles are copyable, so they can be handed out
to different parts of the system that need to manage the channel.
As long as handles exist, the channel remains open. When the last handle is
released or destroyed, the channel's lifecycle is controlled by the active
senders and receivers. As long as at least one sender and one receiver
exists, the channel is open. When either side fully hangs up, the channel
closes. Receivers can drain the channel of all buffered messages even after
the senders have hung up.
Since channel handles keep a channel alive, it is important to get rid of
them when they're no longer needed. The ``Release()`` function is used for
this purpose. It is common to immediately release the handle returned by a
channel creation function if you don't need to close the channel early.
-----------------------------
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 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
.. dropdown:: Hint
.. literalinclude:: ./checkpoint1/vending_machine.h
:language: cpp
:start-at: class DispenserTask
:end-before: } // namespace codelab
:linenos:
#. 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.
.. dropdown:: Hint
.. literalinclude:: ./checkpoint1/vending_machine.cc
:language: cpp
:start-at: pw::async2::Poll<> DispenserTask::DoPend
:end-before: } // namespace codelab
:linenos:
-------------------------
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.
#. 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.
.. dropdown:: Hint
.. literalinclude:: ./checkpoint1/vending_machine.h
:language: cpp
:start-at: class VendingMachineTask
:end-before: class DispenserTask
:linenos:
:emphasize-lines: 25-26,34-35
#. 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.
.. dropdown:: Hint
.. literalinclude:: ./checkpoint1/vending_machine.cc
:language: cpp
:start-at: case kAwaitingSelection: {
:end-before: pw::async2::Poll<> DispenserTask::DoPend
:linenos:
:emphasize-lines: 8-11,22-33,35-53
------------------
Test the dispenser
------------------
#. Run the app:
.. code-block:: console
bazelisk run //pw_async2/codelab
#. Press :kbd:`c` :kbd:`Enter` to input a coin.
#. Press :kbd:`1` :kbd:`Enter` to make a selection.
#. Press :kbd:`i` :kbd:`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.
.. code-block:: none
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
:cc:`Timeout <pw::async2::Timeout>` helper which works with various common
future types.
#. Prepare ``DispenserTask`` to support timeouts in ``vending_machine.h``:
* Include the header that provides timeout-related features:
* ``pw_async2/future_timeout.h``
.. dropdown:: Hint
.. literalinclude:: ./checkpoint2/vending_machine.h
:language: cpp
:start-after: // the License.
:end-before: namespace codelab {
:linenos:
:emphasize-lines: 11
* 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
:cc:`ValueFutureWithTimeout <pw::async2::ValueFutureWithTimeout>`:
.. dropdown:: Hint
.. literalinclude:: ./checkpoint2/vending_machine.h
:language: cpp
:start-at: class DispenserTask
:end-before: } // namespace codelab
:linenos:
:emphasize-lines: 17,27,31
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`` to
``kDispensing``), initialize ``drop_future_`` using
:cc:`Timeout <pw::async2::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.
.. dropdown:: Hint
.. literalinclude:: ./checkpoint2/vending_machine.cc
:language: cpp
:start-at: pw::async2::Poll<> DispenserTask::DoPend
:end-at: } // namespace codelab
:linenos:
:emphasize-lines: 21-22,27-31,48-59
--------------------------------
Test the dispenser with timeouts
--------------------------------
#. Run the app:
.. code-block:: console
bazelisk run //pw_async2/codelab
#. Press :kbd:`c` :kbd:`Enter` to input a coin.
# Press :kbd:`1` :kbd:`Enter` to make a selection.
#. Wait for the timeout.
After 5 seconds you should see a message about the dispense failing.
.. code-block:: none
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 :kbd:`i` :kbd:`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.
.. raw:: html
<div style="margin-bottom:1em">
<button id="dispense">DISPENSE PRIZE</button>
<p id="prize" style="display:none">🍭</p>
</div>
<script>
document.querySelector("#dispense").addEventListener("click", (e) => {
e.target.disabled = true;
document.querySelector("#prize").style.display = "block";
});
</script>
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, :ref:`we would be happy to chat
<module-pw_async2-codelab-help>`.
.. _module-pw_async2-codelab-step5-checkpoint:
----------
Checkpoint
----------
At this point, your code should look similar to the files below.
.. tab-set::
.. tab-item:: main.cc
.. literalinclude:: ./checkpoint2/main.cc
:language: cpp
:start-after: // the License.
.. tab-item:: vending_machine.cc
.. literalinclude:: ./checkpoint2/vending_machine.cc
:language: cpp
:start-after: // the License.
.. tab-item:: vending_machine.h
.. literalinclude:: ./checkpoint2/vending_machine.h
:language: cpp
:start-after: // the License.