Basics before starting with Robotics — Part 8

Actions in ROS

Talha Hanif Butt
13 min readJul 31, 2022

--

I started off with Hello World program in ROS, followed it with Topics and Services. Today I will discuss Actions in ROS.

While services are handy for simple get/set interactions like querying status and managing configuration, they don’t work well when you need to initiate a long-running task. For example, imagine commanding a robot to drive to some distant location; call it goto_position . The robot will require significant time (seconds, minutes, perhaps longer) to do so, with the exact amount of time impossible to know in advance, since obstacles may arise that result in a longer path.

Imagine what a service interface to goto_position might look like to the caller: you send a request containing the goal location, then you wait for an indeterminate amount of time to receive the response that tells you what happened. While waiting, your calling program is forced to block, you have no information about the robot’s progress toward the goal, and you can’t cancel or change the goal. To address these shortcomings, ROS provides actions.

ROS actions are the best way to implement interfaces to time-extended, goal-oriented behaviors like goto_position . While services are synchronous, actions are asynchronous. Similar to the request and response of a service, an action uses a goal to initiate a behavior and sends a result when the behavior is complete. But the action further uses feedback to provide updates on the behavior’s progress toward the goal and also allows for goals to be canceled. Actions are themselves implemented using topics. An action is essentially a
higher-level protocol that specifies how a set of topics (goal, result, feedback, etc.) should be used in combination.

Using an action interface to goto_position , you send a goal, then move on to other tasks while the robot is driving. Along the way, you receive periodic progress updates (distance traveled, estimated time to goal, etc.), culminating in a result message (did the robot make it to the goal or was it forced to give up?). And if something more important comes up, you can at any time cancel the goal and send the robot somewhere else.

Actions require only a little more effort to define and use than do services, and they provide a lot more power and flexibility.

Defining an Action

The first step in creating a new action is to define the goal, result, and feedback message formats in an action definition file, which by convention has the suffix .action. The .action file format is similar to the .srv format used to define services, just with an additional field. And, as with services, each field within an .action file will become its own message.

As a simple example, let’s define an action that acts like a timer. We want this timer to count down, signaling us when the specified time has elapsed. Along the way, it should tell us periodically how much time is left. When it’s done, it should tell us how much time actually elapsed.

An action definition that will satisfy these requirements is as follows:

duration time_to_wait
— -
duration time_elapsed
uint32 updates_sent
— -
duration time_elapsed
duration time_remaining

Just like with service-definition files, we use three dashes ( — — ) as the separator between the parts of the definition. While service definitions have two parts (request and response), action definitions have three parts (goal, result, and feedback).

The action file Timer.action should be placed in a directory called action within a ROS package.

With the definition file in the right place, we need to run catkin_make to create the code and class definitions that we will actually use when interacting with the action, just like we did for new services. To get catkin_make to generate this code, we need to add some lines to the CMakeLists.txt file. First, add actionlib_msgs to the () call (in addition to
any other packages that are already there):

Then, use the add_action_files() call to tell catkin about the action files you want to compile:

Make sure you list the dependencies for your actions. You also need to explicitly list actionlib_msgs as a dependency in order for actions to compile properly:

Finally, add actionlib_msgs as a dependency for catkin:

With all of this information in place, running catkin_make in the top level of our catkin workspace does quite a bit of extra work for us. Our Timer.action file is processed to produce several message-definition files: TimerAction.msg, TimerActionFeedback.msg, TimerActionGoal.msg, TimerActionResult.msg, TimerFeedback.msg, TimerGoal.msg, and TimerResult.msg. These messages are used to implement the action client/server protocol, which, as mentioned previously, is built on top of ROS topics. The generated message definitions are in turn processed by the message generator to produce corresponding class definitions. Most of the time, you’ll use only a few of those classes, as you’ll see in the following examples.

Implementing a Basic Action Server

Now that we have a definition of the goal, result, and feedback for the timer action, we’re ready to write the code that implements it. Like topics and services, actions are a callback-based mechanism, with your code being invoked as a result of receiving messages from another node.

The easiest way to build an action server is to use the SimpleActionServer class from the actionlib package. We’ll start by defining only the callback that will be invoked when a new goal is sent by an action client. In that callback, we’ll do the work of the timer, then return a result when we’re done. We’ll add feedback reporting in the next step.

Following shows the code for our simple action server:

simple_action_server.py

#!/usr/bin/env python2
import rospy
import time
import actionlib
from my_awesome_code.msg import TimerAction, TimerGoal, TimerResult

def do_timer(goal):

start_time = time.time()
time.sleep(goal.time_to_wait.to_sec())
result = TimerResult()
result.time_elapsed = rospy.Duration.from_sec(time.time() — start_time)
result.updates_sent = 0
server.set_succeeded(result)

rospy.init_node(‘timer_action_server’)
server = actionlib.SimpleActionServer(‘timer’, TimerAction, do_timer, False)
server.start()
rospy.spin()

Let’s step through the key parts of the code. First we import the standard Python time package, which we’ll use for the timer functionality of our server. We also import the ROS actionlib package that provides the SimpleActionServer class that we’ll be using. Finally, we import some of the message classes that were autogenerated from our Timer.action file:

Next, we define do_timer() , the function that will be invoked when we receive a new goal. In this function, we handle the new goal in-place and set a result before returning. The type of the goal argument that is passed to do_timer() is TimerGoal , which corresponds to the goal part of Timer.action. We save the current time, using the standard Python time.time() function, then sleep for the time requested in the goal, converting the time_to_wait field from a ROS duration to seconds:

The next step is to build up the result message, which will be of type TimerResult ; this corresponds to the result part of Timer.action. We fill in the time_elapsed field by subtracting our saved start time from the current time, and converting the result to a ROS duration. We set updates_sent to zero, because we didn’t send any updates along the way (we’ll add that part shortly):

Our final step in the callback is to tell the SimpleActionServer that we successfully achieved the goal by calling set_succeeded() and passing it the result. For this simple server, we always succeed; we’ll address failure cases later in this chapter:

Back in the global scope, we initialize and name our node as usual, then create a SimpleActionServer . The first constructor argument for SimpleActionServer is the server’s name, which will determine the namespace into which its constituent topics will be advertised; we’ll use timer . The second argument is the type of the action that the server will be handling, which in our case is TimerAction . The third argument is the goal callback, which is the function do_timer() that we defined earlier. Finally, we pass False to disable autostarting the server. Having created the action server, we explicitly start() it, then go into the usual ROS spin() loop to wait for goals to arrive:

Checking That Everything Works as Expected

Now that we have implemented the action server, we can do a couple of checks to ensure that it’s working as expected. Start up a roscore and then run the action server:

rosrun my_awesome_code simple_action_server.py

Using an Action

The easiest way to use an action is via the SimpleActionClient class from the actionlib package. Following shows a simple client that sends a goal to our action server and waits for the result.

simple_action_client.py

#!/usr/bin/env python2

import rospy
import actionlib
from my_awesome_code.msg import TimerAction, TimerGoal, TimerResult

rospy.init_node(‘timer_action_client’)
client = actionlib.SimpleActionClient(‘timer’, TimerAction)
client.wait_for_server()
goal = TimerGoal()
goal.time_to_wait = rospy.Duration.from_sec(5.0)
client.send_goal(goal)
client.wait_for_result()
print(‘Time elapsed: %f’%(client.get_result().time_elapsed.to_sec()))

Let’s step through the key parts of the code. Following the usual imports and initialization of our ROS node, we create a SimpleActionClient . The first constructor argument is the name of the action server, which the client will use to determine the topics that it will use when communicating with the server. This name must match the one that we used in creating the server, which is timer . The second argument is the type of the action, which must also match the server: TimerAction.

Having created the client, we tell it to wait for the action server to come up, which it does by checking for the five advertised topics that we saw earlier when testing the server. Similar to rospy.wait_for_service() , which we used to wait for a service to be ready, SimpleActionClient.wait_for_server() will block until the server is ready:

Now we create a goal of type TimerGoal and fill in the amount of time we want the timer to wait, which is five seconds. Then we send the goal, which causes the transmission of the goal message to the server:

Next, we wait for a result from the server. If things are working properly, we expect to block here for about five seconds. After the result comes in, we use get_result() to retrieve it from within the client object and print out the time_elapsed field that was reported by the server:

Checking That Everything Works as Expected

Now that we have implemented the action client, we can get to work. Make sure that your roscore and action server are still running, then run the action client:

rosrun my_awesome_code simple_action_client.py

Between the invocation of the client and the printing of the result data, you should see a delay of approximately five seconds, as requested. The time elapsed should be slightly more than five seconds, because a call to time.sleep() will usually take a little longer than requested.

Implementing a More Sophisticated Action Server

So far, actions look a lot like services, just with more configuration and setup.
Now it’s time to exercise the asynchronous aspects of actions that set them apart from services. We’ll start on the server side, making some changes that demonstrate how to abort a goal, how to handle a goal preemption request, and how to provide feedback while pursuing a goal. Following shows the code for our improved action server.

fancy_action_server.py

Because we will be providing feedback, we add TimerFeedback to the list of message types that we import:

from my_awesome_code.msg import TimerAction, TimerGoal, TimerResult, TimerFeedback

Stepping inside our do_timer() callback, we add a variable that will keep track of how many times we publish feedback:

update_count = 0

Next, we add some error checking. We don’t want this timer to be used for long waits, so we check whether the requested time_to_wait is greater than 60 seconds, and if so, we explicitly abort the goal by calling set_aborted() . This call sends a message to the client notifying it that the goal has been aborted. Like with set_succeeded() , we include a result; doing this is optional, but a good idea if possible. We also include a status string to help the client understand what happened; in this case, we aborted because the requested wait was too long. Finally, we return from the callback because we’re done with this goal:

Now that we’re past the error check, instead of just sleeping for the requested time in one shot, we’re going to loop, sleeping in increments. This allows us to do things while we’re working toward the goal, such as checking for preemption and providing feedback:

while (time.time() — start_time) < goal.time_to_wait.to_sec():

In the loop, we first check for preemption by asking the server is_preempt_requested() . This function will return True if the client has requested that we stop pursuing the goal (this could also happen if a second client sends us a new goal). If so, similar to the abort case, we fill in a result and provide a status string, this time calling set_preempted() :

Next we send feedback, using the type TimerFeedback , which corresponds to the feedback part of Timer.action. We fill in the time_elapsed and time_remaining fields, then call publish_feedback() to send it to the client. We also increment update_count to reflect the fact that we sent another update:

Then we sleep a little and loop. Sleeping for a fixed amount of time here is not the right way to implement a timer, as we could easily end up sleeping longer than requested, but it makes for a simpler example:

time.sleep(1.0)

Exiting the loop means that we’ve successfully slept for the requested duration, so it’s time to notify the client that we’re done. This step is very similar to the simple action server, except that we fill in the updates_sent field and add a status string:

Using the More Sophisticated Action

Now we’ll modify the action client to try out the new capabilities that we added to the action server: we’ll process feedback, preempt a goal, and trigger an abort. Following shows the code for our improved action client.

fancy_action_client.py

#!/usr/bin/env python2
import rospy
import time
import actionlib
from my_awesome_code.msg import TimerAction, TimerGoal, TimerResult, TimerFeedback
def feedback_cb(feedback):
print(‘[Feedback] Time elapsed: %f’%(feedback.time_elapsed.to_sec()))
print(‘[Feedback] Time remaining: %f’%(feedback.time_remaining.to_sec()))
rospy.init_node(‘timer_action_client’)
client = actionlib.SimpleActionClient(‘timer’, TimerAction)
client.wait_for_server()
goal = TimerGoal()
goal.time_to_wait = rospy.Duration.from_sec(5.0)
# Uncomment this line to test server-side abort:
#goal.time_to_wait = rospy.Duration.from_sec(500.0)
client.send_goal(goal, feedback_cb=feedback_cb)
# Uncomment these lines to test goal preemption:
#time.sleep(3.0)
#client.cancel_goal()
client.wait_for_result()
print(‘[Result] State: %d’%(client.get_state()))
print(‘[Result] Status: %s’%(client.get_goal_status_text()))
print(‘[Result] Time elapsed: %f’%(client.get_result().time_elapsed.to_sec()))
print(‘[Result] Updates sent: %d’%(client.get_result().updates_sent))

We define a callback, feedback_cb() , that will be invoked when we receive a feedback message. In this callback we just print the contents of the feedback:

We register our feedback callback by passing it as the feedback_cb keyword argument when calling send_goal() :

client.send_goal(goal, feedback_cb=feedback_cb)

After receiving the result, we print a little more information to show what happened. The get_state() function returns the state of the goal, which is an enumeration that is defined in actionlib_msgs/GoalStatus . While there are 10 possible states, in this example we’ll encounter only three: PREEMPTED=2 , SUCCEEDED=3 , and ABORTED=4 . We also print the status text that was included by the server with the result:

Checking That Everything Works as Expected

Let’s try out our new server and client. As before, start up a roscore , then run the server:

rosrun my_awesome_code fancy_action_server.py

In another terminal, run the client:

rosrun my_awesome_code fancy_action_client.py

Everything works as expected: while waiting, we receive one feedback update per second, then we receive a successful result ( SUCCEEDED=3 ).
Now let’s try preempting a goal. In the client, following the call to send_goal() , uncomment these two lines, which will cause the client to sleep briefly, then request that the server preempt the goal:

Run the client again:

rosrun my_awesome_code fancy_action_client.py

That’s the behavior we expect: the server pursues the goal, providing feedback, until we send the cancellation request, after which we receive the result confirming the preemption ( PREEMPTED=2 ).

Now let’s trigger a server-side abort. In the client, uncomment this line to change the requested wait time from 5 seconds to 500 seconds:

Run the client again:

rosrun my_awesome_code fancy_action_client.py

As expected, the server immediately aborted the goal ( ABORTED=4 ).

Comparison of topics, services, and actions

Taken together, these features of actions make them well suited to many aspects of robot programming. It’s common in a robotics application to implement time-extended, goal-seeking behaviors, whether it’s goto_position or clean_the_house . Any time you need to be able trigger a behavior, actions are probably the right tool for the job. In fact, any time that you’re using a service, it’s worth considering replacing it with an action; actions require a bit more code to use, but in return they’re much more powerful and extensible
than services. We’ll see many examples in future chapters where actions provide rich but easy-to-use interfaces to some pretty complex behaviors.

That’s it for now. See you later.

References

https://github.com/StevenShiChina/books/blob/master/Programming.Robots.with.ROS.A.Practical.Introduction.to.the.Robot.Operating.System.pdf

--

--

Talha Hanif Butt

PhD Student -- Signal and Systems Engineering, Halmstad University, Volvo Trucks http://pk.linkedin.com/in/talhahanifbutt