[ROS in 5 mins] 033 – How to create a ROS Action Server

[ROS in 5 mins] 033 - How to create a ROS Action Server

Written by Ruben Alves

03/09/2018

Hello ROS Developers!

In today’s video we are going learn how to create a ROS Action Server.

The commands used in this video can be typed on your own computer if you already has ROS installed, but to make things easier, I’ll use Robot Ignite Academy, which is the best tool if you want to Learn ROS Really Fast.

Before we start, if you are new to ROS, I highly recommend you taking any of the following courses on Robot Ignite Academy:

Ok, let’s get started.

Create your own ROS Action Messages

When creating an Action Server you can use existing Action Messages, but since we are here to learn, we are going to create and use our own ROS Messages. We already have a post (https://www.theconstruct.ai/ros-5-mins-032-compile-ros-action-messages/) showing how to create your own ROS Messages and compile them. Please refer to it before proceeding in this post, since we will use the package and messages created there.

Creating the ROS Action Server

If you followed the post aforementioned on how to create your own ROS Messages, with the tree . command on the ~/catkin_ws/src folder you should have a package called actions_tutorial with the WashTheDishes.action action message,  something like the output below:

user:~/catkin_ws/src$ tree .
.
├── actions_tutorial
│ ├── action
│ │ └── WashTheDishes.action
│ ├── CMakeLists.txt
│ ├── package.xml
│ └── src
└── CMakeLists.txt -> /opt/ros/kinetic/share/catkin/cmake/toplevel.cmake

3 directories, 4 files

In order to create the action server, let’s create a folder called scripts on our actions_tutorial package. On the scripts folder let’s create a file called action_server.py and make it executable. Let’s use the commands below to accomplish that:

cd ~/catkin_ws/src/actions_tutorial
mkdir scripts
cd scripts/
touch action_server.py
chmod +x action_server.py

Let’s insert the code below on our action_server.pyand see what it really does:

#! /usr/bin/env python

import rospy
import actionlib
from actions_tutorial.msg import WashTheDishesAction, WashTheDishesFeedback, WashTheDishesResult


class ActionServer():

    def __init__(self):
        self.a_server = actionlib.SimpleActionServer(
            "wash_dishes_as", WashTheDishesAction, execute_cb=self.execute_cb, auto_start=False)
        self.a_server.start()

    def execute_cb(self, goal):

        success = True
        last_dish_washed = ''
        feedback = WashTheDishesFeedback()
        result = WashTheDishesResult()
        rate = rospy.Rate(1)

        for i in range(0, goal.number_of_minutes):
            if self.a_server.is_preempt_requested():
                self.a_server.set_preempted()
                success = False
                break

            last_dish_washed = 'bowl-' + str(i)
            feedback.last_dish_washed = last_dish_washed
            result.dishes_washed.append(last_dish_washed)
            self.a_server.publish_feedback(feedback)
            rate.sleep()

        if success:
            self.a_server.set_succeeded(result)


if __name__ == "__main__":
    rospy.init_node("action_server")
    s = ActionServer()
    rospy.spin()

Let’s check what the first lines do in our code:

#! /usr/bin/env python

import rospy
import actionlib
from actions_tutorial.msg import WashTheDishesAction, WashTheDishesFeedback, WashTheDishesResult

In the first line, we tell Linux that our code is executed with Python. We then import rospy because we are creating our ROS program using python. Additionally, we import actionlib because it is necessary to create our ROS Action Server. Lastly, we import our own Action Messages that will allow our robot to “wash the dishes”.

Let’s now analyze the other lines of code:

class ActionServer():

    def __init__(self):
        self.a_server = actionlib.SimpleActionServer(
            "wash_dishes_as", WashTheDishesAction, execute_cb=self.execute_cb, auto_start=False)
        self.a_server.start()

Here we create our action server class, which we called ActionServer. We then have our default constructor where we define our a_server, which is an instance of actionlib.SimpleActionServer. The first parameter is the name of our server once it is running (we called wash_dishes_as), the second parameter is the specification of our action, in this case, WashTheDishesAction. The execute_cb parameter defines the function that will be executed once a client calls our server.

Now let’s dive into the execute_cb method:

def execute_cb(self, goal):

        success = True
        last_dish_washed = ''
        feedback = WashTheDishesFeedback()
        result = WashTheDishesResult()
        rate = rospy.Rate(1)

        for i in range(0, goal.number_of_minutes):
            if self.a_server.is_preempt_requested():
                self.a_server.set_preempted()
                success = False
                break

            last_dish_washed = 'bowl-' + str(i)
            feedback.last_dish_washed = last_dish_washed
            result.dishes_washed.append(last_dish_washed)
            self.a_server.publish_feedback(feedback)
            rate.sleep()

        if success:
            self.a_server.set_succeeded(result)

Once the action server is called, our execute_cb method is executed with the goal provided by the user. In the first lines, we basically define the variables we are going to use.

Our action is supposed to wash dishes for the number of minutes provided by the user on the goal.number_of_minutes parameter, but since we are not really washing dishes, we are basically iterating on that number and sending a feedback message to the user.

A ROS Action normally takes a long time to finish, so the user can change his mind and cancel it at any time. We check if this is the case with if self.a_server.is_preempt_requested(): . If so, we break the loop and our server finishes its work.

If the user didn’t cancel, we keep doing our job, which will be washing dishes. We fill the feedback and resultvariables with the last dish washed. If you remember the post mentioned at the beginning, we see that our action has int32 number_of_minutes as the goal, string last_dish_washed as the feedback and string[] dishes_washed as a result.

Once the robot worked the for the time asked on the goal.number_of_minutesparameter, we just send the result to the user with self.a_server.set_succeeded(result).

The last part of the code (shown below) we basically instantiate our ROS Node as well as the Action Server. We call ros.spin() to keep our program running forever, until someone kills it with ctrl+c .

if __name__ == "__main__":
    rospy.init_node("action_server")
    s = ActionServer()
    rospy.spin()

Awesome, we now have everything in place. Let’s now run our server and send it a goal.

Running the ROS Action Server

To launch our server, the messages need to be compiled. If you missed this step when creating your message, you can just do it with the commands below:

cd ~/catkin_ws/
catkin_make

If everything went ok, you should have a message at the end saying  [100%] Built target actions_tutorial_generate_messages.

With the messages compiled, we need to source our catkin_ws just to make sure ROS can find our compiled messages. We will do that on the same shell (terminal) used to launch our server.

Ok, let’s launch our server:

source ~/catkin_ws/devel/setup.bash
rosrun actions_tutorial action_server.py

If no error message is shown, our service is running. We can confirm that by running the command rosnode list | grep server which should output /action_server, since is the name we defined on our default constructor.

We can confirm that our node is providing the wash the dishes service with rosnode info /action_server, which should output something like:

user:~$ rosnode info /action_server
--------------------------------------------------------------------------------
Node [/action_server]
Publications:
 * /rosout [rosgraph_msgs/Log]
 * /wash_dishes_as/feedback [actions_tutorial/WashTheDishesActionFeedback]
 * /wash_dishes_as/result [actions_tutorial/WashTheDishesActionResult]
 * /wash_dishes_as/status [actionlib_msgs/GoalStatusArray]

Subscriptions:
 * /clock [rosgraph_msgs/Clock]
 * /wash_dishes_as/cancel [unknown type]
 * /wash_dishes_as/goal [unknown type]

We see that our node publishes feedback, status and result. If we subscribe to the feedback topic we will see the feedback messages published when the user sends a goal. Let’s subscribe with the following command in a different shell: rostopic echo /wash_dishes_as/feedback

We can also see the result once the action is finished by subscribing to the result topic in a different shell. Let’s do it with the next command: rostopic echo /wash_dishes_as/result

With everything in place, we can now send our goal. With the rostopic info /action_server aforementioned, we see that our node is subscribed to the /wash_dishes_as/goal topic. This is exactly the topic we use to send goals.

In order to send a goal we can publish on that topic with rostopic pub /wash_dishes_as/goal TAB+TAB+TAB

By pressing TAB+TAB ROS automatically completes the type of the message as well as its fields. If everything goes ok you should have something like the output below:

rostopic pub /wash_dishes_as/goal actions_tutorial/WashTheDishesActionGoal "header:
  seq: 0
  stamp:
    secs: 0
    nsecs: 0
  frame_id: ''
goal_id:
  stamp:
    secs: 0
    nsecs: 0
  id: ''
goal:
  number_of_minutes: 0"

You can see that in the last line we have number_of_minutes: 0. With the left arrow key you can reach that field and change its value. Let’s change it to 5 and after the " we write --once to publish the message only once. After that we press enter. To make sure we are doing right, before pressing enter we should have something like:

rostopic pub /wash_dishes_as/goal actions_tutorial/WashTheDishesActionGoal "header:
  seq: 0
  stamp:
    secs: 0
    nsecs: 0
  frame_id: ''
goal_id:
  stamp:
    secs: 0
    nsecs: 0
  id: ''
goal:
  number_of_minutes: 5" --once

You will see nothing on the shell you execute this command, but if you go to the shell were you executedrostopic echo /wash_dishes_as/feedback, you should see 5 feedback messages saying that the dishes are being washed.

Also, if you go to the shell where you typed rostopic echo /wash_dishes_as/result, you should see the result message, like the one below:

user:~$ rostopic echo  /wash_dishes_as/result
WARNING: no messages received and simulated time is active.
Is /clock being published?
header:
  seq: 2
  stamp:
    secs: 4155
    nsecs: 895000000
  frame_id: ''
status:
  goal_id:
    stamp:
      secs: 4150
      nsecs: 891000000
    id: "/action_server-2-4150.891000000"
  status: 3
  text: ''
result:
  dishes_washed: [bowl-0, bowl-1, bowl-2, bowl-3, bowl-4]
---

Note that on the last line we have the name of the dishes washed: dishes_washed: [bowl-0, bowl-1, bowl-2, bowl-3, bowl-4]

A video to make your life easier

Congratulations. You now have an action server fully operational. If you liked the post, great, but if you want also to watch a video showing all the process described in this post, we have it here:

Now that we know how to create an Action Server, let’s learn how to create an Action Client? If you are interested, please have a look at the next post: https://www.theconstruct.ai/ros-5-mins-035-create-ros-action-client/

To conclude, if you liked the video, please give us a thumbs up on YouTube. Additionally, should you have any feedback messages, please leave them on the comments section of the video. You can also subscribe to our youtube channel and press the bell to be notified about the videos we publish every day.

Keep pushing our ROS Learning.

Masterclass 2023 batch2 blog banner

Check Out These Related Posts

129. ros2ai

129. ros2ai

I would like to dedicate this episode to all the ROS Developers who believe that ChatGPT or...

read more

0 Comments

Submit a Comment

Your email address will not be published.

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Pin It on Pinterest

Share This