[ROS2 How-to] #2 – Create a ROS2 action server
What we are going to learn
- – How to create a custom action message
- How to create an action server
List of resources used in this post
- Use the rosject: https://app.theconstructsim.com/#/l/4a1c58c5/
- The Construct: https://app.theconstructsim.com/
- ROS2 Courses –▸
- ROS2 Basics in 5 Days (Python): https://app.theconstructsim.com/#/Course/73
- ROS2 Basics in 5 Days (C++): https://app.theconstructsim.com/#/Course/61
- ROS2 Navigation training: https://www.theconstruct.ai/ros2-navigation-training/
What is a ROS2 Action
Let’s assume you wish to wash your clothing. There are two possible ways you could go about it:
- Go to the Laundry service provider
- Put your clothes to wash.
- Wait until the clothes are washed.
- Get your clothes.
- If you have a washing machine at home:
- Put your clothes to wash
- Instead of waiting, you can do other things and leave the watching machine doing its jobs
- Check once in a while if the clothes are finished
- Do other things.
- Clothes are washed.
Option 1 is a blocking activity because you have to wait (in theory not able to do anything else) for the clothes to be washed, while option 2 is non-blocking because you can do some other things while your clothes are being washed.
This non-blocking is what defines an Action. If ROS2 Services are for instant request-responses, an Action is a task that may take a lot of time to be finished, and in the meantime, a robot (or you) is free to do other things and is also able to constantly check the status of the action.
Opening the rosject
In order to learn how to create an and use an Action Server in ROS2, we need to have ROS2 installed in our system, and it is also useful to have some simulations. To make your life easier, we already prepared a rosject with a simulation for that: https://app.theconstructsim.com/#/l/4a1c58c5/.
You can download the rosject on your own computer if you want to work locally, but just by copying the rosject (clicking the link), you will have a setup already prepared for you.
After the rosject has been successfully copied to your own area, you should see a Run button. Just click that button to launch the rosject (below you have a rosject example).
After pressing the Run button, you should have the rosject loaded. Let’s now head to the next section to really get some real practice.
Launching the simulation
The rosject we provided contains the packages needed to run a TurtleBot3 simulation in ROS2. The TurtleBot3 has the following sensors:
- Lidar
- IMU
Feel free to use this rosject to test your mobile robot programs.
The rosject is structured the following way:
turtlebot3_ws
: this workspace contains the TurtleBot3 packages provided by ROBOTIS. Don’t modify this unless you know what you are doing and want to change something from the simulation- Use this workspace to develop your programs
Assuming you have opened the rosject by clicking the Run button, we can launch the simulation with:
cd
source turtlebot3_ws/install/setup.bash
source turtlebot3_ws/install/setup.bash
export TURTLEBOT3_MODEL=burger
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py
After a few seconds, the simulation should have opened automatically:
In case the simulation does not pop up automatically, you can easily click the Open Gazebo button like in the example below (bear in mind that the simulation below is not the one used in this tutorial. Its main purpose is to show the Open Gazebo button):
Creating our ROS2 package (later used to create our Action Server)
Let’s create our ROS2 Package. For that, let’s start by opening a new terminal:
In the terminal that was just open, by running the “ls” command you can see that we have at least the following folders:
ros2_ws turtlebot3_ws
The turtlebot3_ws contains the simulation, and the ros2_ws is where we are going to place our code.
Before you continue, it is worth mentioning that in the rosject that we shared with you, the custom_interfaces package that we are going to create here already exists. We are going to create it here basically for learning purposes. You would actually not need it since the package was already created for you:
Let’s create a package named custom_interfaces with the action_msgs std_msgs rosids_default_generators packages as dependencies:
cd ~/ros2_ws/src/ ros2 pkg create custom_interfaces2 --build-type ament_cmake --dependencies action_msgs std_msgs rosidl_default_generators
If everything went ok, you should see the following:
going to create a new package package name: custom_interfaces destination directory: /home/user/ros2_ws/src package format: 3 version: 0.0.0 description: TODO: Package description maintainer: ['user <user@todo.todo>'] licenses: ['TODO: License declaration'] build type: ament_cmake dependencies: ['action_msgs', 'std_msgs', 'rosidl_default_generators'] creating folder ./custom_interfaces creating ./custom_interfaces/package.xml creating source and include folder creating folder ./custom_interfaces/src creating folder ./custom_interfaces/include/custom_interfaces2 creating ./custom_interfaces/CMakeLists.txt
After the package was created, let’s create a folder called action:
mkdir -p custom_interfaces/action/
and also create the action/Patrol.action file.
touch custom_interfaces/action/Patrol.action
This is the file/Interface that we will use in our Action Server for patrolling.
Let’s now open that Patrol.action file. You can open it in the Code Editor. If you do not know how to open the Code Editor, please check the image below:
You can now open the custom_interfaces/action/Patrol.action file and paste the following content on it:
#Goal float32 radius --- #Result bool success --- #Feedback float32 time_left
Now, to be able to compile our message file, we have to open the custom_interfaces/CMakeLists.txt file and paste the following content around line 14:
set(action_files "action/Patrol.action" ) rosidl_generate_interfaces(${PROJECT_NAME} ${action_files} DEPENDENCIES action_msgs std_msgs )
cmake_minimum_required(VERSION 3.8) project(custom_interfaces) if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic) endif() # find dependencies find_package(ament_cmake REQUIRED) find_package(action_msgs REQUIRED) find_package(std_msgs REQUIRED) find_package(rosidl_default_generators REQUIRED) set(action_files "action/Patrol.action" ) rosidl_generate_interfaces(${PROJECT_NAME} ${action_files} DEPENDENCIES action_msgs std_msgs ) if(BUILD_TESTING) find_package(ament_lint_auto REQUIRED) # the following line skips the linter which checks for copyrights # uncomment the line when a copyright and license is not present in all source files #set(ament_cmake_copyright_FOUND TRUE) # the following line skips cpplint (only works in a git repo) # uncomment the line when this package is not in a git repo #set(ament_cmake_cpplint_FOUND TRUE) ament_lint_auto_find_test_dependencies() endif() ament_package()
And for the file custom_interfaces/package.xml we also have to add the following code before the <export> tag:
<depend>builtin_interfaces</depend> <exec_depend>rosidl_default_runtime</exec_depend> <member_of_group>rosidl_interface_packages</member_of_group>
In the end, our package.xml would look like the following:
<?xml version="1.0"?> <?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?> <package format="3"> <name>custom_interfaces</name> <version>0.0.0</version> <description>TODO: Package description</description> <maintainer email="user@todo.todo">user</maintainer> <license>TODO: License declaration</license> <buildtool_depend>ament_cmake</buildtool_depend> <depend>action_msgs</depend> <depend>std_msgs</depend> <depend>rosidl_default_generators</depend> <test_depend>ament_lint_auto</test_depend> <test_depend>ament_lint_common</test_depend> <depend>builtin_interfaces</depend> <exec_depend>rosidl_default_runtime</exec_depend> <member_of_group>rosidl_interface_packages</member_of_group> <export> <build_type>ament_cmake</build_type> </export> </package>
Please make sure you save the files with Ctrl+S after making the modifications.
Compiling our custom Action interface
Now that we defined our Custom Action interface, let’s compile it.
Let’s go to the first terminal we opened previously and run the following commands:
cd ~/ros2_ws/ colcon build
The package should have been compiled with no errors:
Starting >>> custom_interfaces Finished <<< custom_interfaces [9.53s] Summary: 1 package finished [9.53s]
Let’s now make sure ROS2 can find our Action interface:
source install/setup.bash ros2 interface show custom_interfaces/action/Patrol
It should show:
#Goal float32 radius --- #Result bool success --- #Feedback float32 time_left
So far so good. ROS is able to find our custom interface.
The time has now come for us to create the Action Server.
Creating our ROS2 Action Server
Let’s create a different package for the Action Server, just to keep things separated. Since we are not doing to create Interfaces in this new package, just use existing interfaces, let’s use the ament_python build type. Again, bear in mind that if you are using the rosject that we provided, the package already exists in the ~/ros2_ws/src folder:
cd ~/ros2_ws/src/ ros2 pkg create --build-type ament_python patrol_action_server --dependencies rclpy geometry_mgs custom_interfaces
The logs should be similar to the following:
going to create a new package package name: patrol_action_server destination directory: /home/user/ros2_ws/src package format: 3 version: 0.0.0 description: TODO: Package description maintainer: ['user <user@todo.todo>'] licenses: ['TODO: License declaration'] build type: ament_python dependencies: ['rclpy', 'geometry_mgs', 'custom_interfaces'] creating folder ./patrol_action_server creating ./patrol_action_server/package.xml creating source folder creating folder ./patrol_action_server/patrol_action_server2 creating ./patrol_action_server/setup.py creating ./patrol_action_server/setup.cfg creating folder ./patrol_action_server/resource creating ./patrol_action_server/resource/patrol_action_server creating ./patrol_action_server/patrol_action_server/__init__.py creating folder ./patrol_action_server/test creating ./patrol_action_server/test/test_copyright.py creating ./patrol_action_server/test/test_flake8.py creating ./patrol_action_server/test/test_pep257.py
Now that our package is created, let’s create a file patrol_action_server.py that will have the code of our Action Server:
touch patrol_action_server/patrol_action_server/patrol_action_server.py
Let’s now open that file using the Code Editor, and paste the following content to it:
#!/usr/bin/env python3 # # Copyright 2019 ROBOTIS CO., LTD. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # Authors: Ryan Shim, Gilbert import math import time import rclpy from geometry_msgs.msg import Twist from rclpy.action import ActionServer from rclpy.action import CancelResponse from rclpy.action import GoalResponse from rclpy.callback_groups import ReentrantCallbackGroup from rclpy.duration import Duration from rclpy.node import Node from rclpy.qos import QoSProfile from rclpy.executors import MultiThreadedExecutor from custom_interfaces.action import Patrol class Turtlebot3PatrolServer(Node): def __init__(self): super().__init__('turtlebot3_patrol_server') self.goal = Patrol.Goal() qos = QoSProfile(depth=10) # Initialise publishers self.cmd_vel_pub = self.create_publisher(Twist, 'cmd_vel', qos) # Initialise servers self._action_server = ActionServer( self, Patrol, 'patrol', execute_callback=self.execute_callback, callback_group=ReentrantCallbackGroup(), goal_callback=self.goal_callback, cancel_callback=self.cancel_callback) self.get_logger().info("Turtlebot3 patrol action server has been initialised.") def destroy(self): self._action_server.destroy() super().destroy_node() def goal_callback(self, goal_request): # Accepts or rejects a client request to begin an action self.get_logger().info('Received goal request :)') self.goal = goal_request return GoalResponse.ACCEPT def cancel_callback(self, goal_handle): # Accepts or rejects a client request to cancel an action self.get_logger().info('Received cancel request :(') return CancelResponse.ACCEPT async def execute_callback(self, goal_handle): self.get_logger().info('Executing goal...') radius = self.goal.radius # unit: m speed = 0.5 # unit: m/s feedback_msg = Patrol.Feedback() total_driving_time = 2 * math.pi * radius / speed feedback_msg.time_left = total_driving_time last_time = self.get_clock().now() # Start executing an action while (feedback_msg.time_left > 0): if goal_handle.is_cancel_requested: goal_handle.canceled() self.get_logger().info('Goal canceled') return Patrol.Result() curr_time = self.get_clock().now() duration = Duration() duration = (curr_time - last_time).nanoseconds / 1e9 # unit: s feedback_msg.time_left = total_driving_time - duration self.get_logger().info('Time left until the robot stops: {0}'.format(feedback_msg.time_left)) goal_handle.publish_feedback(feedback_msg) # Give vel_cmd to Turtlebot3 twist = Twist() twist = self.drive_circle(radius, speed) self.cmd_vel_pub.publish(twist) # Process rate time.sleep(0.010) # unit: s # When the action is completed twist = Twist() self.cmd_vel_pub.publish(twist) goal_handle.succeed() result = Patrol.Result() result.success = True self.get_logger().info('Returning result: {0}'.format(result.success)) return result def drive_circle(self, radius, velocity): self.twist = Twist() self.linear_velocity = velocity # unit: m/s self.angular_velocity = self.linear_velocity / radius # unit: rad/s self.twist.linear.x = self.linear_velocity self.twist.angular.z = self.angular_velocity return self.twist def main(args=None): rclpy.init(args=args) patrol_action_server = Turtlebot3PatrolServer() # Use a MultiThreadedExecutor to enable processing goals concurrently executor = MultiThreadedExecutor() rclpy.spin(patrol_action_server, executor=executor) patrol_action_server.destroy() rclpy.shutdown() if __name__ == '__main__': main()
entry_points={ 'console_scripts': [ 'patrol_action_server_exe = patrol_action_server.patrol_action_server:main', ], },
In the end, the complete ~/ros2_ws/src/patrol_action_server/setup.py would be as follows:
from setuptools import setup package_name = 'patrol_action_server' setup( name=package_name, version='0.0.0', packages=[package_name], data_files=[ ('share/ament_index/resource_index/packages', ['resource/' + package_name]), ('share/' + package_name, ['package.xml']), ], install_requires=['setuptools'], zip_safe=True, maintainer='user', maintainer_email='user@todo.todo', description='TODO: Package description', license='TODO: License declaration', tests_require=['pytest'], entry_points={ 'console_scripts': [ 'patrol_action_server_exe = patrol_action_server.patrol_action_server:main', ], }, )
Compiling our ROS2 Action Server
With everything in place, we compile our package just as before:
cd ~/ros2_ws/ colcon build
Starting >>> custom_interfaces Finished <<< custom_interfaces [9.53s] Starting >>> patrol_action_server Finished <<< patrol_action_server [5.33s] Summary: 2 packages finished [15.2s]
source install/setup.bash ros2 run patrol_action_server patrol_action_server_exe
[INFO] [1651528559.914166370] [turtlebot3_patrol_server]: Turtlebot3 patrol action server has been initialised
Calling our ROS2 Action Server
Ok, if you did not kill the Action Server launched in the previous section, please open a second terminal that we will use to call the Action Server.
With “ros2 node list” we should be able to find our node running:
ros2 node list /turtlebot3_patrol_server
ros2 action list /patrol
ros2 action send_goal --feedback /patrol custom_interfaces/action/Patrol radius:\ 0.5\
Waiting for an action server to become available... Sending goal: radius: 0.5 Goal accepted with ID: dd32bc835d7a4ef5ae854d0bfb4b119f Feedback: time_left: 6.2831525802612305 Feedback: time_left: 6.271763801574707 Feedback: time_left: 6.260392665863037 Feedback: time_left: 6.2484917640686035 Feedback: time_left: 6.237414836883545 Feedback: time_left: 6.2265496253967285 Feedback: time_left: 6.215761661529541 ... ^CCanceling goal... Feedback: time_left: 5.634908676147461 Goal canceled.
Remember that you can easily cancel the call to the action server by pressing CTRL+C.
If you look at the simulation after sending a goal to the Action Server, you should see the robot spinning around 0.5 meters.
Congratulations. You now know how to create a ROS2 Action Server from scratch. If you want more details about the code of the Action Server, please check the video in the next section.
Youtube video
So this is the post for today. Remember that we have the live version of this post on YouTube. If you liked the content, please consider subscribing to our youtube channel. We are publishing new content ~every day.
Keep pushing your ROS Learning.
Related Courses & Training
If you want to learn more about ROS and ROS2, we recommend the following courses:
- ROS2 Basics in 5 Days (Python): https://app.theconstructsim.com/#/Course/73
- ROS2 Basics in 5 Days (C++): https://app.theconstructsim.com/#/Course/61