5.10. Actions#
The last type of connection that we are going to cover are Actions.
Actions are a combination of Services and Topics. They are initiated by calls to an action server, just like services are initiated by calls to a regular server. However, action services provide intermittent feedback by publishing to a topic that the action client can subscribe to. So, when should you use each?
5.10.1. Topics vs Services vs Actions#
Topics, services, and actions can all be used to transfer information between nodes, but they have different use cases.
Topics should be used for passing streams of information that are always relevant. For example, when a turtle exists in turtlesim, it is constantly publishing its current position over the /turtle1/pose topic, and is constantly reading a desired velocity over the /turtle1/cmd_vel. These topics constantly accepting and publishing data, and are valid as long as turtle1 exists. Note that publishing to cmd_vel, despite only representing information, still causes the turtle to move. Topics can be used to cause something to happen.
Services should be used for requesting something quick to happen. The turtlesim node exposes several services that can be requested through service calls, like /spawn to spawn a turtle, or /reset to reset the simulation. These happen almost instantaneously, and there is no need for feedback while the server is executing the action. Services can return info, like in the case of /spawn, which returns the name of the created turtle.
Actions should be used for requesting something that might take a while, that would require feedback. turtlesim exposes one action, turtle1/rotate_absolute. This causes the turtle to turn to a position, but since the turtle can’t turn instantaneously, an action is used to keep the client updated on the turning progress.
5.10.2. Asynchronous Programming#
5.10.2.1. Async Methods#
In the services tutorial, we glossed over an important method call: call_async(). We used that method to asynchronously call our services, as opposed to using synchronous calls via call().
A synchronous call is like a regular function call, where everything happens in the order you expect it to. Take this modified function from the services activity:
self.set_pen_client = self.create_client(SetPen, '/turtle1/set_pen')
def set_pen(self, r, g, b):
req = SetPen.Request()
# Set R, G, B, and width
return self.set_pen_client.call(req)
This makes a synchronous call to the set_pen server from our set_pen client. The last line will request that the server change the pen color, and it will not move on until the service has come back and said either: “The pen color has been set,” or “The request failed.” This is called a blocking operation, since it blocks all code after it until it finishes. Compare that to the function that we actually used:
def set_pen(self, r, g, b):
req = SetPen.Request()
# Set R, G, B, and width
return self.set_pen_client.call_async(req)
This makes an asynchronous call to the set_pen server. The last line will request that the server sets the pen color, but it won’t actually wait for the server to confirm that the pen color has been set. This allows your code to do other things while ROS2 is busy passing messages around. This is a non-blocking operation. Importantly, synchronous calls cannot be used inside callbacks. This is because callbacks are called by ROS2, and synchronous calls are blocking. If you ask ROS2 to make a call, but then tell it not to do anything until the call is complete, ROS2 can’t complete your call and will get stuck!
Asynchronous calls are usually good to use, but they introduce some complexity into your program. If you need to get the result of a call, you can’t be sure that the call has actually been completed yet! This is where futures come in.
5.10.2.2. Futures#
Futures in rclpy represent the outcome of some task. Usually when you create or recieve a Future, that task is not yet done. Futures allow you to check if a task is done by calling the done() method, and get the result of the task using the result() method.
For services, futures will resolve to the result of the service. So, if a service called /spawn returns a response withstring name, you can get name with the following line:
self.spawn_client = self.create_client(Spawn, '/spawn')
self.spawn_future = self.spawn_client.call_async(Spawn.Request())
self.create_timer(1, self.print_name)
def print_name(self):
if(self.spawn_future.done()):
print(self.spawn_future.result().name)
When print_name() is called, if /spawn has completed, it will print the name that was returned. Note that we use create_timer to run print_name() every 1 second. Alternatively, add_done_callback can be used to trigger a callback once, as soon as the result resolves. We don’t need to check if the future is done, since if the callback is called, we know that the future finished. In a more complex example, we might want some error checking on the future, but for simplicity we are excluding that check for now.
self.spawn_client = self.create_client(Spawn, '/spawn')
self.spawn_future = self.spawn_client.call_async(Spawn.Request())
self.spawn_future.add_done_callback(self.print_name)
def print_name(self, future: Future):
print(future.result().name)
The above examples show futures as used with services. Actions futures are similar, but have a different result type.
5.10.3. Programming with Client Actions#
5.10.3.1. Send Goal#
Like services, actions can be called both synchronously and asynchronously. However, for purposes of this tutorial, actions should almost never be called synchronously. We have established that actions are a good fit for long-running actions, and we also saw how synchronous calls were blocking calls. If we make a blocking call that takes a long time in the main loop of the node, we’re going to run into unexpected behavior! Additionally, synchronous action calls function almost identically to service calls, in that we aren’t able to make use of the feedback part of actions. We will simply have a service that takes a long time to execute. Still, for completeness, calling actions synchronously can be done like this:
self.rotate_client = ActionClient(self, RotateAbsolute, '/turtle1/rotate_absolute')
def rotate_to_goal(self, angle):
goal = RotateAbsolute.Goal()
goal.theta = angle
return self.rotate_client.send_goal(goal)
Most of the time we should call actions asynchronously. Actions can be called asynchronously like this:
def rotate_to_goal(self, angle):
goal = RotateAbsolute.Goal()
goal.theta = angle
return self.rotate_client.send_goal_async(goal)
Like with sevices, this function will return a Future. However, getting feedback and the final result is a little more complicated.
Actions operate in 4 stages:
We, the action client, make a request to the action server. At this point, the future is not done, and the result is
None.The action server recieves and processes our request, and responds. At this point, the future is done, and the result is a
GoalHandlewith anAcceptedorExecutingstatus.The action server periodically sends feedback to our action client if we gave it a callback function. At this point, the future is done and the result is a
GoalHandlewith anAcceptedorExecutingstatus.The action server finishes the action. At this point, the future is done and the result is a
GoalHandlewith theSucceededstatus.
You may have noticed that the future finished at step 2! As a reminder, with services we were able to use the results right after our future finished. With actions, the future only tells us whether or not the server has accepted our request, not if it is done or not. That means once we have our GoalHandle, we need to use that to determine if our action has finished or not.
5.10.3.2. Goal Handles#
Goal handles are resources we can use to interact with the action while it is executing. Goal handles have the following important properties:
5.10.3.2.1. Status#
Access with: my_goal_handle.status
status. Status tells you important information about the progress of your goal. The valid states are:
Unknown (STATUS_UNKNOWN): This is not a valid state. If you encounter this, something has gone wrong.
Accepted (STATUS_ACCEPTED): The action server has acknowledged your request to do something, and will start executing it when it can.
Executing (STATUS_EXECUTING): The action server is currently executing your request, but has not yet finished.
Canceling (STATUS_CANCELING): You, the client, have requested that your request be canceled. The server is canceling your request, but has not yet finished.
Succeeded (STATUS_SUCCEEDED): The action server has already completed your request, and is doing something else.
Canceled (STATUS_CANCELED): You, the client, have requested that your request be canceled. The server has already canceled your request, and is doing something else.
Aborted (STATUS_ABORTED): Something went wrong, and the server stopped executing your goal and will not finish. Unlike canceling, you did not ask the server to stop executing the goal.
status is an integer, and can be from 0-6. The numbers in the above states correspond to what they mean, but you should use the constants defined in rclpy. For example:
from rclpy.action.client import Goal
# ...
if my_goal_handle.status == GoalStatus.STATUS_SUCCEEDED:
pass # Do something!
5.10.3.2.2. Get Result#
Access with my_goal_handle.get_result() or my_goal_handle.get_result_async(). For reasons discussed before you should almost always use my_goal_handle.get_result_async().
Calling get_result() will stop the node from doing anything until the action completes, and then will return the final result, just like calling result on a Future.
Calling get_result_async() will return a future. This means that, over the course of an action call, you will deal with 2 futures: On send_goal_async which finishes when the action server accepts the request, and on get_result_async which finishes when the action server finishes the action. Don’t get confused, the future returned by send_goal_async won’t tell you nearly enough information about the status of your goal! The result of this new future will be the final ROS2 result message from the server.
5.10.3.2.3. Cancel Goal#
Access with my_goal_handle.cancel_goal() or my_goal_handle.cancel_goal_async(). Prefer the async version of the method.
Canceling the goal will cause the action server to stop executing the goal the client asked it to execute. This will result in the canceling or canceled status.
5.10.3.3. Feedback#
The main advantage actions have over services is their ability to send a stream of feedback while the server is executing the goal. You can define a feedback with send_goal_async(), but not with send_goal(). That’s because if you use the synchronous method send_goal(), your node can’t do anything else until the goal has finished, including process any feedback!
You can define a function to process feedback as soon as it arrives by specifying the feedback function as the second parameter to send_goal_async.
def feedback_callback(self, feedback_msg):
feedback = feedback_msg.feedback # Take the feedback out of the message! This property is always called `feedback`.
do_something(feedback.result_field) # Use the result! The name of `result_field` is whatever the actual message result property is called.
send_goal_async(goal, feedback_callback)
Now, whenever the server sends feedback, the feedback_callback will be run and it will process the feedback immediately.