ROS2-Humble Guide - Tutorial - Beginner: Client Libraries¶
I Using colcon To Build Packages¶
Background¶
Info
colcon
is an iteration on the ROS build tools catkin_make
, catkin_make_isolated
, catkin_tools
and ament_tools
. For more information on the design of colcon see this document. The source code can be found in the colcon GitHub organization.
Prerequisites¶
1 Install colcon¶
2 Install ROS 2¶
Follow the instructions.
Basics¶
Info
A ROS workspace is a directory with a particular structure. Commonly there is a src
subdirectory. Inside that subdirectory is where the source code of ROS packages will be located. Typically the directory starts otherwise empty.
colcon does out of source builds. By default it will create the following directories as peers of the src
directory:
The build
directory will be where intermediate files are stored. For each package a subfolder will be created in which e.g. CMake is being invoked.
The install
directory is where each package will be installed to. By default each package will be installed into a separate subdirectory.
The log
directory contains various logging information about each colcon invocation.
Note
Compared to catkin there is no devel
directory.
1 Create a workspace¶
2 Add some sources¶
Letâs clone the examples repository into the src
directory of the workspace:
3 Source an underlay¶
It is important that we have sourced the environment for an existing ROS 2 installation that will provide our workspace with the necessary build dependencies for the example packages. This is achieved by sourcing the setup script provided by a binary installation or a source installation, ie. another colcon workspace (see Installation). We call this environment an underlay.
Our workspace, ros2_ws, will be an overlay on top of the existing ROS 2 installation. In general, it is recommended to use an overlay when you plan to iterate on a small number of packages, rather than putting all of your packages into the same workspace.
4 Build the workspace¶
Attention
To build packages on Windows you need to be in a Visual Studio environment, see Building the ROS 2 Code for more details.
In the root of the workspace, run colcon build
. Since build types such as ament_cmake
do not support the concept of the devel
space and require the package to be installed, colcon supports the option --symlink-install
. This allows the installed files to be changed by changing the files in the source
space (e.g. Python files or other non-compiled resources) for faster iteration.
5 Run tests¶
To run tests for the packages we just built, run the following:
Remember to use a x64 Native Tools Command Prompt for VS 2019 for executing the following command, as we are going to build a workspace. You also need to specify --merge-install here since we used it for building above.
colcon test --merge-install
6 Source the environment¶
When colcon has completed building successfully, the output will be in the install
directory. Before you can use any of the installed executables or libraries, you will need to add them to your path and library paths. colcon will have generated bash/bat files in the install
directory to help set up the environment. These files will add all of the required elements to your path and library paths as well as provide any bash or shell commands exported by packages.
7 Try a demo¶
With the environment sourced, we can run executables built by colcon. Letâs run a subscriber node from the examples:
In another terminal, letâs run a publisher node (donât forget to source the setup script): You should see messages from the publisher and subscriber with numbers incrementing.Create your own package¶
colcon uses the package.xml
specification defined in REP 149 (format 2 is also supported).
colcon supports multiple build types. The recommended build types are ament_cmake
and ament_python
. Also supported are pure cmake
packages.
An example of an ament_python
build is the ament_index_python package , where the setup.py is the primary entry point for building.
A package such as demo_nodes_cpp uses the ament_cmake
build type, and uses CMake as the build tool.
For convenience, you can use the tool ros2 pkg create
to create a new package based on a template.
Note
For catkin
users, this is the equivalent of catkin_create_package
.
Setup colcon_cd¶
The command colcon_cd
allows you to quickly change the current working directory of your shell to the directory of a package. As an example colcon_cd some_ros_package
would quickly bring you to the directory ~/ros2_ws/src/some_ros_package
.
Depending on the way you installed colcon_cd
and where your workspace is, the instructions above may vary, please refer to the documentation for more details. To undo this in Linux and macOS, locate your systemâs shell startup script and remove the appended source and export commands.
Setup colcon tab completion¶
The command colcon
supports command completion for bash and bash-like shells if the colcon-argcomplete
package is installed.
Depending on the way you installed colcon
and where your workspace is, the instructions above may vary, please refer to the documentation for more details. To undo this in Linux and macOS, locate your systemâs shell startup script and remove the appended source command.
Tips¶
If you do not want to build a specific package place an empty file named COLCON_IGNORE
in the directory and it will not be indexed.
If you want to avoid configuring and building tests in CMake packages you can pass: --cmake-args -DBUILD_TESTING=0
.
If you want to run a single particular test from a package:
II Creating A Workspace¶
Background¶
A workspace is a directory containing ROS 2 packages. Before using ROS 2, itâs necessary to source your ROS 2 installation workspace in the terminal you plan to work in. This makes ROS 2âs packages available for you to use in that terminal.
You also have the option of sourcing an âoverlayâ - a secondary workspace where you can add new packages without interfering with the existing ROS 2 workspace that youâre extending, or âunderlayâ. Your underlay must contain the dependencies of all the packages in your overlay. Packages in your overlay will override packages in the underlay. Itâs also possible to have several layers of underlays and overlays, with each successive overlay using the packages of its parent underlays.
Prerequisites¶
- ROS 2 installation
- colcon installation
- git installation
- turtlesim installation
- Have rosdep installed
- Understanding of basic terminal commands (hereâs a guide for Linux)
- Text editor of your choice
Tasks¶
1 Source ROS 2 environment¶
Your main ROS 2 installation will be your underlay for this tutorial. (Keep in mind that an underlay does not necessarily have to be the main ROS 2 installation.)
Depending on how you installed ROS 2 (from source or binaries), and which platform youâre on, your exact source command will vary:
2 Create a new directory¶
Best practice is to create a new directory for every new workspace. The name doesnât matter, but it is helpful to have it indicate the purpose of the workspace. Letâs choose the directory name ros2_ws
, for âdevelopment workspaceâ:
Another best practice is to put any packages in your workspace into the src directory. The above code creates a src directory inside ros2_ws and then navigates into it.
3 Clone a sample repo¶
Ensure youâre still in the ros2_ws/src
directory before you clone.
In the rest of the beginner developer tutorials, you will create your own packages, but for now you will practice putting a workspace together using existing packages.
If you went through the Beginner: CLI Tools tutorials, youâll be familiar with turtlesim
, one of the packages in ros_tutorials.
A repo can have multiple branches. You need to check out the one that targets your installed ROS 2 distro. When you clone this repo, add the -b argument followed by that branch.
In the ros2_ws/src directory, run the following command:
Now ros_tutorials
is cloned in your workspace. The ros_tutorials
repository contains the turtlesim
package, which weâll use in the rest of this tutorial. The other packages in this repository are not built because they contain a COLCON_IGNORE
file.
So far you have populated your workspace with a sample package, but it isnât a fully-functional workspace yet. You need to resolve the dependencies first and then build the workspace.
4 Resolve dependencies¶
Before building the workspace, you need to resolve the package dependencies. You may have all the dependencies already, but best practice is to check for dependencies every time you clone. You wouldnât want a build to fail after a long wait only to realize that you have missing dependencies.
From the root of your workspace (ros2_ws
), run the following command:
If you installed ROS 2 on Linux from source or the âfatâ archive, you will need to use the rosdep command from their installation instructions. Here are the from-source rosdep section and the âfatâ archive rosdep section.
If you already have all your dependencies, the console will return:
Packages declare their dependencies in the package.xml file (you will learn more about packages in the next tutorial). This command walks through those declarations and installs the ones that are missing. You can learn more about rosdep in another tutorial (coming soon).
5 Build the workspace with colcon¶
From the root of your workspace (ros2_ws), you can now build your packages using the command:
The console will return the following message:
Note
Other useful arguments for colcon build: --packages-up-to builds the package you want, plus all its dependencies, but not the whole workspace (saves time) --symlink-install saves you from having to rebuild every time you tweak python scripts --event-handlers console_direct+ shows console output while building (can otherwise be found in the log directory)
Once the build is finished, enter the command in the workspace root (~/ros2_ws):
And you will see that colcon has created new directories: build, install, log and src. The install directory is where your workspaceâs setup files are, which you can use to source your overlay.
6 Source the overlay¶
Before sourcing the overlay, it is very important that you open a new terminal, separate from the one where you built the workspace. Sourcing an overlay in the same terminal where you built, or likewise building where an overlay is sourced, may create complex issues.
In the new terminal, source your main ROS 2 environment as the âunderlayâ, so you can build the overlay âon top ofâ it:
Go into the root of your workspace (ros2_ws):
In the root, source your overlay:
Note
Sourcing the local_setup of the overlay will only add the packages available in the overlay to your environment. setup sources the overlay as well as the underlay it was created in, allowing you to utilize both workspaces.
So, sourcing your main ROS 2 installationâs setup and then the ros2_ws overlayâs local_setup, like you just did, is the same as just sourcing ros2_wsâs setup, because that includes the environment of its underlay.
Now you can run the turtlesim package from the overlay:
But how can you tell that this is the overlay turtlesim running, and not your main installationâs turtlesim?Letâs modify turtlesim in the overlay so you can see the effects:
- You can modify and rebuild packages in the overlay separately from the underlay.
- The overlay takes precedence over the underlay.
7 Modify the overlay¶
You can modify turtlesim
in your overlay by editing the title bar on the turtlesim window. To do this, locate the turtle_frame.cpp
file in ~/ros2_ws/src/ros_tutorials/turtlesim/src
. Open turtle_frame.cpp
with your preferred text editor.
On line 52 you will see the function setWindowTitle("TurtleSim")
;. Change the value "TurtleSim"
to "MyTurtleSim"
, and save the file.
Return to the first terminal where you ran colcon build
earlier and run it again.
Return to the second terminal (where the overlay is sourced) and run turtlesim again:
You will see the title bar on the turtlesim window now says âMyTurtleSimâ.
Even though your main ROS 2 environment was sourced in this terminal earlier, the overlay of your ros2_ws
environment takes precedence over the contents of the underlay.
To see that your underlay is still intact, open a brand new terminal and source only your ROS 2 installation. Run turtlesim again:
You can see that modifications in the overlay did not actually affect anything in the underlay.Summary¶
In this tutorial, you sourced your main ROS 2 distro install as your underlay, and created an overlay by cloning and building packages in a new workspace. The overlay gets prepended to the path, and takes precedence over the underlay, as you saw with your modified turtlesim.
Using overlays is recommended for working on a small number of packages, so you donât have to put everything in the same workspace and rebuild a huge workspace on every iteration.
III Creating A Package¶
Background¶
1 What is a ROS 2 package?¶
A package is an organizational unit for your ROS 2 code. If you want to be able to install your code or share it with others, then youâll need it organized in a package. With packages, you can release your ROS 2 work and allow others to build and use it easily.
Package creation in ROS 2 uses ament
as its build system and colcon
as its build tool. You can create a package using either CMake
or Python
, which are officially supported, though other build types do exist.
2 What makes up a ROS 2 package?¶
ROS 2 Python and CMake packages each have their own minimum required contents:
- CMakeLists.txt file that describes how to build the code within the package
- include/
directory containing the public headers for the package - package.xml file containing meta information about the package
- src directory containing the source code for the package
- package.xml file containing meta information about the package
- resource/
marker file for the package - setup.cfg is required when a package has executables, so ros2 run can find them
- setup.py containing instructions for how to install the package
- a directory with the same name as your package, used by ROS 2 tools to find your package, contains init.py
The simplest possible package may have a file structure that looks like:
3 Packages in a workspace¶
A single workspace can contain as many packages as you want, each in their own folder. You can also have packages of different build types in one workspace (CMake, Python, etc.). You cannot have nested packages.
Best practice is to have a src
folder within your workspace, and to create your packages in there. This keeps the top level of the workspace âcleanâ.
A trivial workspace might look like:
workspace_folder/
src/
cpp_package_1/
CMakeLists.txt
include/cpp_package_1/
package.xml
src/
py_package_1/
package.xml
resource/py_package_1
setup.cfg
setup.py
py_package_1/
...
cpp_package_n/
CMakeLists.txt
include/cpp_package_n/
package.xml
src/
Prerequisites¶
You should have a ROS 2 workspace after following the instructions in the previous tutorial. You will create your package in this workspace.
Tasks¶
1 Create a package¶
First, source your ROS 2 installation.
Letâs use the workspace you created in the previous tutorial, ros2_ws, for your new package.
Make sure you are in the src folder before running the package creation command.
The command syntax for creating a new package in ROS 2 is:
For this tutorial, you will use the optional argument --node-name which creates a simple Hello World type executable in the package.
Enter the following command in your terminal:
You will now have a new folder within your workspaceâs src directory called my_package.
After running the command, your terminal will return the message:
going to create a new package
package name: my_package
destination directory: /home/user/ros2_ws/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['<name> <email>']
licenses: ['TODO: License declaration']
build type: ament_cmake
dependencies: []
node_name: my_node
creating folder ./my_package
creating ./my_package/package.xml
creating source and include folder
creating folder ./my_package/src
creating folder ./my_package/include/my_package
creating ./my_package/CMakeLists.txt
creating ./my_package/src/my_node.cpp
going to create a new package
package name: my_package
destination directory: /home/user/ros2_ws/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['<name> <email>']
licenses: ['TODO: License declaration']
build type: ament_python
dependencies: []
node_name: my_node
creating folder ./my_package
creating ./my_package/package.xml
creating source folder
creating folder ./my_package/my_package
creating ./my_package/setup.py
creating ./my_package/setup.cfg
creating folder ./my_package/resource
creating ./my_package/resource/my_package
creating ./my_package/my_package/__init__.py
creating folder ./my_package/test
creating ./my_package/test/test_copyright.py
creating ./my_package/test/test_flake8.py
creating ./my_package/test/test_pep257.py
creating ./my_package/my_package/my_node.py
You can see the automatically generated files for the new package.
2 Build a package¶
Putting packages in a workspace is especially valuable because you can build many packages at once by running colcon build
in the workspace root. Otherwise, you would have to build each package individually.
Return to the root of your workspace:
Now you can build your packages:
Recall from the last tutorial that you also have the ros_tutorials
packages in your ros2_ws
. You might have noticed that running colcon build
also built the turtlesim
package. Thatâs fine when you only have a few packages in your workspace, but when there are many packages, colcon build can take a long time.
To build only the my_package package next time, you can run:
3 Source the setup file¶
To use your new package and executable, first open a new terminal and source your main ROS 2 installation.
Then, from inside the ros2_ws directory, run the following command to source your workspace:
Now that your workspace has been added to your path, you will be able to use your new packageâs executables.
4 Use the package¶
To run the executable you created using the --node-name
argument during package creation, enter the command:
Which will return a message to your terminal:
5 Examine package contents¶
Inside ros2_ws/src/my_package
, you will see the files and folders that ros2 pkg create
automatically generated:
my_node.cpp
is inside the src
directory. This is where all your custom C++ nodes will go in the future. 6 Customize package.xml¶
You may have noticed in the return message after creating your package that the fields description
and license
contain TODO
notes. Thatâs because the package description and license declaration are not automatically set, but are required if you ever want to release your package. The maintainer
field may also need to be filled in.
From ros2_ws/src/my_package
, open package.xml
using your preferred text editor:
<?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>my_package</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>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>
<?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>my_package</name>
<version>0.0.0</version>
<description>TODO: Package description</description>
<maintainer email="user@todo.todo">user</maintainer>
<license>TODO: License declaration</license>
<test_depend>ament_copyright</test_depend>
<test_depend>ament_flake8</test_depend>
<test_depend>ament_pep257</test_depend>
<test_depend>python3-pytest</test_depend>
<export>
<build_type>ament_python</build_type>
</export>
</package>
Input your name and email on the maintainer
line if it hasnât been automatically populated for you. Then, edit the description line to summarize the package:
license
line. You can read more about open source licenses here. Since this package is only for practice, itâs safe to use any license. Weâll use Apache License 2.0
: Donât forget to save once youâre done editing. Below the license tag, you will see some tag names ending with _depend
. This is where your package.xml
would list its dependencies on other packages, for colcon to search for. my_package
is simple and doesnât have any dependencies, but you will see this space being utilized in upcoming tutorials.
you are done
The setup.py
file contains the same description, maintainer and license fields as package.xml
, so you need to set those as well. They need to match exactly in both files. The version and name (package_name
) also need to match exactly, and should be automatically populated in both files.
Open setup.py with your preferred text editor.
from setuptools import setup
package_name = 'my_py_pkg'
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='TODO',
maintainer_email='TODO',
description='TODO: Package description',
license='TODO: License declaration',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'my_node = my_py_pkg.my_node:main'
],
},
)
Donât forget to save the file.
Summary¶
Youâve created a package to organize your code and make it easy to use for others.
Your package was automatically populated with the necessary files, and then you used colcon to build it so you can use its executables in your local environment.
IV Writing A Simple Publisher And Subscriber (C++)¶
Background¶
Nodes are executable processes that communicate over the ROS graph. In this tutorial, the nodes will pass information in the form of string messages to each other over a topic. The example used here is a simple âtalkerâ and âlistenerâ system; one node publishes data and the other subscribes to the topic so it can receive that data.
The code used in these examples can be found here.
Prerequisites¶
In previous tutorials, you learned how to create a workspace and create a package.
Tasks¶
1 Create a package¶
Open a new terminal and source your ROS 2 installation so that ros2
commands will work.
Navigate into the ros2_ws
directory created in a previous tutorial.
Recall that packages should be created in the src
directory, not the root of the workspace. So, navigate into ros2_ws/src
, and run the package creation command:
Your terminal will return a message verifying the creation of your package cpp_pubsub
and all its necessary files and folders.
Navigate into ros2_ws/src/cpp_pubsub/src
. Recall that this is the directory in any CMake package where the source files containing executables belong.
2 Write the publisher node¶
Download the example talker code by entering the following command:
Now there will be a new file named publisher_member_function.cpp. Open the file using your preferred text editor.
#include <chrono>
#include <functional>
#include <memory>
#include <string>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
/* This example creates a subclass of Node and uses std::bind() to register a
* member function as a callback from the timer. */
class MinimalPublisher : public rclcpp::Node
{
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
{
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
timer_ = this->create_wall_timer(
500ms, std::bind(&MinimalPublisher::timer_callback, this));
}
private:
void timer_callback()
{
auto message = std_msgs::msg::String();
message.data = "Hello, world! " + std::to_string(count_++);
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
publisher_->publish(message);
}
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
size_t count_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalPublisher>());
rclcpp::shutdown();
return 0;
}
2.1 Examine the code¶
The top of the code includes the standard C++ headers you will be using. After the standard C++ headers is the rclcpp/rclcpp.hpp
include which allows you to use the most common pieces of the ROS 2 system. Last is std_msgs/msg/string.hpp
, which includes the built-in message type you will use to publish data.
#include <chrono>
#include <functional>
#include <memory>
#include <string>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
package.xml
and CMakeLists.txt
, which youâll do in the next section. The next line creates the node class MinimalPublisher
by inheriting from rclcpp::Node
. Every this in the code is referring to the node.
minimal_publisher
and initializes count_
to 0. Inside the constructor, the publisher is initialized with the String
message type, the topic name topic
, and the required queue size to limit messages in the event of a backup. Next, timer_
is initialized, which causes the timer_callback
function to be executed twice a second. public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
{
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
timer_ = this->create_wall_timer(
500ms, std::bind(&MinimalPublisher::timer_callback, this));
}
timer_callback
function is where the message data is set and the messages are actually published. The RCLCPP_INFO
macro ensures every published message is printed to the console. private:
void timer_callback()
{
auto message = std_msgs::msg::String();
message.data = "Hello, world! " + std::to_string(count_++);
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
publisher_->publish(message);
}
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
size_t count_;
MinimalPublisher
class is main
, where the node actually executes. rclcpp::init
initializes ROS 2, and rclcpp::spin
starts processing data from the node, including callbacks from the timer. int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalPublisher>());
rclcpp::shutdown();
return 0;
}
2.2 Add dependencies¶
Navigate one level back to the ros2_ws/src/cpp_pubsub
directory, where the CMakeLists.txt
and package.xml
files have been created for you.
Open package.xml
with your text editor.
As mentioned in the previous tutorial, make sure to fill in the
<description>Examples of minimal publisher/subscriber using rclcpp</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache License 2.0</license>
This declares the package needs rclcpp
and std_msgs
when its code is built and executed.
Make sure to save the file.
2.3 CMakeLists.txt¶
Now open the CMakeLists.txt
file. Below the existing dependency find_package(ament_cmake REQUIRED)
, add the lines:
talker
so you can run your node using ros2 run: add_executable(talker src/publisher_member_function.cpp)
ament_target_dependencies(talker rclcpp std_msgs)
Finally, add the install(TARGETS...)
section so ros2 run
can find your executable:
You can clean up your CMakeLists.txt
by removing some unnecessary sections and comments, so it looks like this:
cmake_minimum_required(VERSION 3.5)
project(cpp_pubsub)
# Default to C++14
if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 14)
endif()
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
add_executable(talker src/publisher_member_function.cpp)
ament_target_dependencies(talker rclcpp std_msgs)
install(TARGETS
talker
DESTINATION lib/${PROJECT_NAME})
ament_package()
You could build your package now, source the local setup files, and run it, but letâs create the subscriber node first so you can see the full system at work.
3 Write the subscriber node¶
Return to ros2_ws/src/cpp_pubsub/src
to create the next node. Enter the following code in your terminal:
Check to ensure that these files exist:
Open the subscriber_member_function.cpp with your text editor.
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using std::placeholders::_1;
class MinimalSubscriber : public rclcpp::Node
{
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
}
private:
void topic_callback(const std_msgs::msg::String & msg) const
{
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg.data.c_str());
}
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalSubscriber>());
rclcpp::shutdown();
return 0;
}
3.1 Examine the code¶
The subscriber nodeâs code is nearly identical to the publisherâs. Now the node is named minimal_subscriber
, and the constructor uses the nodeâs create_subscription
class to execute the callback.
There is no timer because the subscriber simply responds whenever data is published to the topic
topic.
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
}
Recall from the topic tutorial that the topic name and message type used by the publisher and subscriber must match to allow them to communicate.
The topic_callback
function receives the string message data published over the topic, and simply writes it to the console using the RCLCPP_INFO
macro.
The only field declaration in this class is the subscription.
private:
void topic_callback(const std_msgs::msg::String & msg) const
{
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg.data.c_str());
}
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
The main
function is exactly the same, except now it spins the MinimalSubscriber
node. For the publisher node, spinning meant starting the timer, but for the subscriber it simply means preparing to receive messages whenever they come.
Since this node has the same dependencies as the publisher node, thereâs nothing new to add to package.xml
.
3.2 CMakeLists.txt¶
Reopen CMakeLists.txt
and add the executable and target for the subscriber node below the publisherâs entries.
add_executable(listener src/subscriber_member_function.cpp)
ament_target_dependencies(listener rclcpp std_msgs)
install(TARGETS
talker
listener
DESTINATION lib/${PROJECT_NAME})
Make sure to save the file, and then your pub/sub system should be ready.
4 Build and run¶
You likely already have the rclcpp
and std_msgs
packages installed as part of your ROS 2 system. Itâs good practice to run rosdep
in the root of your workspace (ros2_ws
) to check for missing dependencies before building:
Still in the root of your workspace, ros2_ws
, build your new package:
Open a new terminal, navigate to ros2_ws
, and source the setup files:
The terminal should start publishing info messages every 0.5 seconds, like so:
[INFO] [minimal_publisher]: Publishing: "Hello World: 0"
[INFO] [minimal_publisher]: Publishing: "Hello World: 1"
[INFO] [minimal_publisher]: Publishing: "Hello World: 2"
[INFO] [minimal_publisher]: Publishing: "Hello World: 3"
[INFO] [minimal_publisher]: Publishing: "Hello World: 4"
Open another terminal, source the setup files from inside ros2_ws again, and then start the listener node:
The listener will start printing messages to the console, starting at whatever message count the publisher is on at that time, like so:[INFO] [minimal_subscriber]: I heard: "Hello World: 10"
[INFO] [minimal_subscriber]: I heard: "Hello World: 11"
[INFO] [minimal_subscriber]: I heard: "Hello World: 12"
[INFO] [minimal_subscriber]: I heard: "Hello World: 13"
[INFO] [minimal_subscriber]: I heard: "Hello World: 14"
Ctrl+C
in each terminal to stop the nodes from spinning. Summary¶
You created two nodes to publish and subscribe to data over a topic. Before compiling and running them, you added their dependencies and executables to the package configuration files.
V Writing A Simple Publisher And Subscriber (Python)¶
Background¶
In this tutorial, you will create nodes that pass information in the form of string messages to each other over a topic. The example used here is a simple âtalkerâ and âlistenerâ system; one node publishes data and the other subscribes to the topic so it can receive that data.
The code used in these examples can be found here.
Prerequisites¶
In previous tutorials, you learned how to create a workspace and create a package.
A basic understanding of Python is recommended, but not entirely necessary.
Tasks¶
1 Create a package¶
Open a new terminal and source your ROS 2 installation so that ros2
commands will work.
Navigate into the ros2_ws
directory created in a previous tutorial.
Recall that packages should be created in the src
directory, not the root of the workspace. So, navigate into ros2_ws/src
, and run the package creation command:
Your terminal will return a message verifying the creation of your package py_pubsub
and all its necessary files and folders.
2 Write the publisher node¶
Navigate into ros2_ws/src/py_pubsub/py_pubsub
. Recall that this directory is a Python package with the same name as the ROS 2 package itâs nested in.
Download the example talker code by entering the following command:
Now there will be a new file named publisher_member_function.py
adjacent to __init__.py
.
Open the file using your preferred text editor.
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MinimalPublisher(Node):
def __init__(self):
super().__init__('minimal_publisher')
self.publisher_ = self.create_publisher(String, 'topic', 10)
timer_period = 0.5 # seconds
self.timer = self.create_timer(timer_period, self.timer_callback)
self.i = 0
def timer_callback(self):
msg = String()
msg.data = 'Hello World: %d' % self.i
self.publisher_.publish(msg)
self.get_logger().info('Publishing: "%s"' % msg.data)
self.i += 1
def main(args=None):
rclpy.init(args=args)
minimal_publisher = MinimalPublisher()
rclpy.spin(minimal_publisher)
# Destroy the node explicitly
# (optional - otherwise it will be done automatically
# when the garbage collector destroys the node object)
minimal_publisher.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
2.1 Examine the code¶
The first lines of code after the comments import rclpy so its Node class can be used.
The next statement imports the built-in string message type that the node uses to structure the data that it passes on the topic. These lines represent the nodeâs dependencies. Recall that dependencies have to be added topackage.xml
, which youâll do in the next section. Next, the MinimalPublisher
class is created, which inherits from (or is a subclass of) Node
.
super().__init__
calls the Node
classâs constructor and gives it your node name, in this case minimal_publisher
. create_publisher
declares that the node publishes messages of type String
(imported from the std_msgs.msg
module), over a topic named topic
, and that the âqueue sizeâ is 10. Queue size is a required QoS (quality of service) setting that limits the amount of queued messages if a subscriber is not receiving them fast enough.
Next, a timer is created with a callback to execute every 0.5 seconds. self.i
is a counter used in the callback.
def __init__(self):
super().__init__('minimal_publisher')
self.publisher_ = self.create_publisher(String, 'topic', 10)
timer_period = 0.5 # seconds
self.timer = self.create_timer(timer_period, self.timer_callback)
self.i = 0
timer_callback
creates a message with the counter value appended, and publishes it to the console with get_logger().info
.
def timer_callback(self):
msg = String()
msg.data = 'Hello World: %d' % self.i
self.publisher_.publish(msg)
self.get_logger().info('Publishing: "%s"' % msg.data)
self.i += 1
def main(args=None):
rclpy.init(args=args)
minimal_publisher = MinimalPublisher()
rclpy.spin(minimal_publisher)
# Destroy the node explicitly
# (optional - otherwise it will be done automatically
# when the garbage collector destroys the node object)
minimal_publisher.destroy_node()
rclpy.shutdown()
rclpy
library is initialized, then the node is created, and then it âspinsâ the node so its callbacks are called. 2.2 Add dependencies¶
Navigate one level back to the ros2_ws/src/py_pubsub
directory, where the setup.py
, setup.cfg
, and package.xml
files have been created for you.
Open package.xml
with your text editor.
As mentioned in the previous tutorial, make sure to fill in the
<description>Examples of minimal publisher/subscriber using rclpy</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache License 2.0</license>
rclpy
and std_msgs
when its code is executed. Make sure to save the file.
2.3 Add an entry point¶
Open the setup.py
file. Again, match the maintainer
, maintainer_email
, description
and license
fields to your package.xml
:
maintainer='YourName',
maintainer_email='you@email.com',
description='Examples of minimal publisher/subscriber using rclpy',
license='Apache License 2.0',
console_scripts
brackets of the entry_points
field: Donât forget to save. 2.4 Check setup.cfg¶
The contents of the setup.cfg
file should be correctly populated automatically, like so:
You could build your package now, source the local setup files, and run it, but letâs create the subscriber node first so you can see the full system at work.
3 Write the subscriber node¶
Return to ros2_ws/src/py_pubsub/py_pubsub
to create the next node. Enter the following code in your terminal:
3.1 Examine the code¶
Open the subscriber_member_function.py
with your text editor.
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MinimalSubscriber(Node):
def __init__(self):
super().__init__('minimal_subscriber')
self.subscription = self.create_subscription(
String,
'topic',
self.listener_callback,
10)
self.subscription # prevent unused variable warning
def listener_callback(self, msg):
self.get_logger().info('I heard: "%s"' % msg.data)
def main(args=None):
rclpy.init(args=args)
minimal_subscriber = MinimalSubscriber()
rclpy.spin(minimal_subscriber)
# Destroy the node explicitly
# (optional - otherwise it will be done automatically
# when the garbage collector destroys the node object)
minimal_subscriber.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
The callback definition simply prints an info message to the console, along with the data it received. Recall that the publisher defines msg.data = 'Hello World: %d' % self.i
The main
definition is almost exactly the same, replacing the creation and spinning of the publisher with the subscriber.
Since this node has the same dependencies as the publisher, thereâs nothing new to add to package.xml
. The setup.cfg
file can also remain untouched.
3.2 Add an entry point¶
Reopen setup.py
and add the entry point for the subscriber node below the publisherâs entry point. The entry_points
field should now look like this:
entry_points={
'console_scripts': [
'talker = py_pubsub.publisher_member_function:main',
'listener = py_pubsub.subscriber_member_function:main',
],
},
4 Build and run¶
You likely already have the rclpy
and std_msgs
packages installed as part of your ROS 2 system. Itâs good practice to run rosdep
in the root of your workspace (ros2_ws
) to check for missing dependencies before building:
Still in the root of your workspace, ros2_ws, build your new package:
Now run the talker node:
The terminal should start publishing info messages every 0.5 seconds, like so:[INFO] [minimal_publisher]: Publishing: "Hello World: 0"
[INFO] [minimal_publisher]: Publishing: "Hello World: 1"
[INFO] [minimal_publisher]: Publishing: "Hello World: 2"
[INFO] [minimal_publisher]: Publishing: "Hello World: 3"
[INFO] [minimal_publisher]: Publishing: "Hello World: 4"
...
The listener will start printing messages to the console, starting at whatever message count the publisher is on at that time, like so:
[INFO] [minimal_subscriber]: I heard: "Hello World: 10"
[INFO] [minimal_subscriber]: I heard: "Hello World: 11"
[INFO] [minimal_subscriber]: I heard: "Hello World: 12"
[INFO] [minimal_subscriber]: I heard: "Hello World: 13"
[INFO] [minimal_subscriber]: I heard: "Hello World: 14"
...
Ctrl+C
in each terminal to stop the nodes from spinning. Summary¶
You created two nodes to publish and subscribe to data over a topic. Before running them, you added their dependencies and entry points to the package configuration files.
VI Writing A Simple Service And Client (C++)¶
Background¶
When nodes communicate using services, the node that sends a request for data is called the client node, and the one that responds to the request is the service node. The structure of the request and response is determined by a .srv
file.
The example used here is a simple integer addition system; one node requests the sum of two integers, and the other responds with the result.
Prerequisites¶
In previous tutorials, you learned how to create a workspace and create a package.
Tasks¶
1 Create a package¶
Open a new terminal and source your ROS 2 installation so that ros2 commands will work.
Navigate into the ros2_ws
directory created in a previous tutorial.
Recall that packages should be created in the src
directory, not the root of the workspace. Navigate into ros2_ws/src
and create a new package:
ros2 pkg create --build-type ament_cmake --license Apache-2.0 cpp_srvcli --dependencies rclcpp example_interfaces
Your terminal will return a message verifying the creation of your package cpp_srvcli and all its necessary files and folders.
The --dependencies
argument will automatically add the necessary dependency lines to package.xml
and CMakeLists
.txt. example_interfaces
is the package that includes the .srv file
you will need to structure your requests and responses:
The first two lines are the parameters of the request, and below the dashes is the response.
1.1 Update package.xml¶
Because you used the --dependencies
option during package creation, you donât have to manually add dependencies to package.xml
or CMakeLists.txt
.
As always, though, make sure to add the description, maintainer email and name, and license information to package.xml
.
<description>C++ client server tutorial</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache License 2.0</license>
2 Write the service node¶
Inside the ros2_ws/src/cpp_srvcli/src
directory, create a new file called add_two_ints_server.cpp
and paste the following code within:
#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/add_two_ints.hpp"
#include <memory>
void add(const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> request,
std::shared_ptr<example_interfaces::srv::AddTwoInts::Response> response)
{
response->sum = request->a + request->b;
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld" " b: %ld",
request->a, request->b);
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sending back response: [%ld]", (long int)response->sum);
}
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_server");
rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr service =
node->create_service<example_interfaces::srv::AddTwoInts>("add_two_ints", &add);
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add two ints.");
rclcpp::spin(node);
rclcpp::shutdown();
}
2.1 Examine the code¶
The first two #include statements are your package dependencies.
The add
function adds two integers from the request and gives the sum to the response, while notifying the console of its status using logs.
void add(const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> request,
std::shared_ptr<example_interfaces::srv::AddTwoInts::Response> response)
{
response->sum = request->a + request->b;
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld" " b: %ld",
request->a, request->b);
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sending back response: [%ld]", (long int)response->sum);
}
main
function accomplishes the following, line by line: Initializes ROS 2 C++ client library:
Creates a node namedadd_two_ints_server
: Creates a service named add_two_ints
for that node and automatically advertises it over the networks with the &add
method: rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr service =
node->create_service<example_interfaces::srv::AddTwoInts>("add_two_ints", &add);
Prints a log message when itâs ready:
Spins the node, making the service available.2.2 Add executable¶
The add_executable
macro generates an executable you can run using ros2 run
. Add the following code block to CMakeLists.txt
to create an executable named server
:
add_executable(server src/add_two_ints_server.cpp)
ament_target_dependencies(server rclcpp example_interfaces)
ros2 run
can find the executable, add the following lines to the end of the file, right before ament_package(): You could build your package now, source the local setup files, and run it, but letâs create the client node first so you can see the full system at work.
3 Write the client node¶
Inside the ros2_ws/src/cpp_srvcli/src
directory, create a new file called add_two_ints_client.cpp
and paste the following code within:
#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/add_two_ints.hpp"
#include <chrono>
#include <cstdlib>
#include <memory>
using namespace std::chrono_literals;
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
if (argc != 3) {
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "usage: add_two_ints_client X Y");
return 1;
}
std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_client");
rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr client =
node->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints");
auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
request->a = atoll(argv[1]);
request->b = atoll(argv[2]);
while (!client->wait_for_service(1s)) {
if (!rclcpp::ok()) {
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");
return 0;
}
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");
}
auto result = client->async_send_request(request);
// Wait for the result.
if (rclcpp::spin_until_future_complete(node, result) ==
rclcpp::FutureReturnCode::SUCCESS)
{
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Sum: %ld", result.get()->sum);
} else {
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Failed to call service add_two_ints");
}
rclcpp::shutdown();
return 0;
}
3.1 Examine the code¶
Similar to the service node, the following lines of code create the node and then create the client for that node:
std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_client");
rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr client =
node->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints");
Next, the request is created. Its structure is defined by the .srv
file mentioned earlier.
auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
request->a = atoll(argv[1]);
request->b = atoll(argv[2]);
The while
loop gives the client 1 second to search for service nodes in the network. If it canât find any, it will continue waiting.
Then the client sends its request, and the node spins until it receives its response, or fails.
3.2 Add executable¶
Return to CMakeLists.txt
to add the executable and target for the new node. After removing some unnecessary boilerplate from the automatically generated file, your CMakeLists.txt should look like this:
cmake_minimum_required(VERSION 3.5)
project(cpp_srvcli)
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(example_interfaces REQUIRED)
add_executable(server src/add_two_ints_server.cpp)
ament_target_dependencies(server rclcpp example_interfaces)
add_executable(client src/add_two_ints_client.cpp)
ament_target_dependencies(client rclcpp example_interfaces)
install(TARGETS
server
client
DESTINATION lib/${PROJECT_NAME})
ament_package()
4 Build and run¶
Itâs good practice to run rosdep in the root of your workspace (ros2_ws) to check for missing dependencies before building:
Navigate back to the root of your workspace, ros2_ws, and build your new package:
Open a new terminal, navigate to ros2_ws, and source the setup files:
Now run the server node:
The terminal should print a message saying itâs ready to add two integers.
Open another terminal, source the setup files from inside ros2_ws again. Start the client node, followed by any two integers separated by a space:
If you chose 2 and 3, for example, the client would receive a response like this:
Return to the terminal where your service node is running. You will see that it published log messages when it received the request and the data it received, and the response it sent back:
Enter Ctrl+C
in each terminal to stop the nodes from spinning.
Summary¶
You created two nodes to request and respond to data over a service. You added their dependencies and executables to the package configuration files so that you could build and run them, and see a service/client system at work.
VII Writing A Simple Service And Client (Python)¶
Background¶
When nodes communicate using services, the node that sends a request for data is called the client node, and the one that responds to the request is the service node. The structure of the request and response is determined by a .srv file.
The example used here is a simple integer addition system; one node requests the sum of two integers, and the other responds with the result.
Prerequisites¶
In previous tutorials, you learned how to create a workspace and create a package.
Tasks¶
1 Create a package¶
Open a new terminal and source your ROS 2 installation so that ros2
commands will work.
Navigate into the ros2_ws
directory created in a previous tutorial.
Recall that packages should be created in the src
directory, not the root of the workspace. Navigate into ros2_ws/src
and create a new package:
ros2 pkg create --build-type ament_python --license Apache-2.0 py_srvcli --dependencies rclpy example_interfaces
Your terminal will return a message verifying the creation of your package py_srvcli
and all its necessary files and folders.
The --dependencies
argument will automatically add the necessary dependency lines to package.xml
. example_interfaces
is the package that includes the .srv file you will need to structure your requests and responses:
1.1 Update package.xml¶
Because you used the --dependencies
option during package creation, you donât have to manually add dependencies to package.xml
.
As always, though, make sure to add the description, maintainer email and name, and license information to package.xml
.
<description>Python client server tutorial</description>
<maintainer email="you@email.com">Your Name</maintainer>
<license>Apache License 2.0</license>
1.2 Update setup.py¶
Add the same information to the setup.py
file for the maintainer
, maintainer_email
, description
and license
fields:
maintainer='Your Name',
maintainer_email='you@email.com',
description='Python client server tutorial',
license='Apache License 2.0',
2 Write the service node¶
Inside the ros2_ws/src/py_srvcli/py_srvcli directory, create a new file called service_member_function.py and paste the following code within:
from example_interfaces.srv import AddTwoInts
import rclpy
from rclpy.node import Node
class MinimalService(Node):
def __init__(self):
super().__init__('minimal_service')
self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback)
def add_two_ints_callback(self, request, response):
response.sum = request.a + request.b
self.get_logger().info('Incoming request\na: %d b: %d' % (request.a, request.b))
return response
def main():
rclpy.init()
minimal_service = MinimalService()
rclpy.spin(minimal_service)
rclpy.shutdown()
if __name__ == '__main__':
main()
2.1 Examine the code¶
The first import statement imports the AddTwoInts service type from the example_interfaces package. The following import statement imports the ROS 2 Python client library, and specifically the Node class
The MinimalService class constructor initializes the node with the name minimal_service. Then, it creates a service and defines the type, name, and callback.
def __init__(self):
super().__init__('minimal_service')
self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback)
def add_two_ints_callback(self, request, response):
response.sum = request.a + request.b
self.get_logger().info('Incoming request\na: %d b: %d' % (request.a, request.b))
return response
Finally, the main class initializes the ROS 2 Python client library, instantiates the MinimalService class to create the service node and spins the node to handle callbacks.
2.2 Add an entry point¶
To allow the ros2 run
command to run your node, you must add the entry point to setup.py
(located in the ros2_ws/src/py_srvcli
directory).
Add the following line between the 'console_scripts'
: brackets:
3 Write the client node¶
Inside the ros2_ws/src/py_srvcli/py_srvcli
directory, create a new file called client_member_function.py
and paste the following code within:
import sys
from example_interfaces.srv import AddTwoInts
import rclpy
from rclpy.node import Node
class MinimalClientAsync(Node):
def __init__(self):
super().__init__('minimal_client_async')
self.cli = self.create_client(AddTwoInts, 'add_two_ints')
while not self.cli.wait_for_service(timeout_sec=1.0):
self.get_logger().info('service not available, waiting again...')
self.req = AddTwoInts.Request()
def send_request(self, a, b):
self.req.a = a
self.req.b = b
self.future = self.cli.call_async(self.req)
rclpy.spin_until_future_complete(self, self.future)
return self.future.result()
def main():
rclpy.init()
minimal_client = MinimalClientAsync()
response = minimal_client.send_request(int(sys.argv[1]), int(sys.argv[2]))
minimal_client.get_logger().info(
'Result of add_two_ints: for %d + %d = %d' %
(int(sys.argv[1]), int(sys.argv[2]), response.sum))
minimal_client.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
3.1 Examine the code¶
As with the service code, we first import the necessary libraries.
The MinimalClientAsync class constructor initializes the node with the name minimal_client_async. The constructor definition creates a client with the same type and name as the service node. The type and name must match for the client and service to be able to communicate. The while loop in the constructor checks if a service matching the type and name of the client is available once a second. Finally it creates a new AddTwoInts request object.
def __init__(self):
super().__init__('minimal_client_async')
self.cli = self.create_client(AddTwoInts, 'add_two_ints')
while not self.cli.wait_for_service(timeout_sec=1.0):
self.get_logger().info('service not available, waiting again...')
self.req = AddTwoInts.Request()
def send_request(self, a, b):
self.req.a = a
self.req.b = b
self.future = self.cli.call_async(self.req)
rclpy.spin_until_future_complete(self, self.future)
return self.future.result()
Finally we have the main method, which constructs a MinimalClientAsync object, sends the request using the passed-in command-line arguments, and logs the results.
def main():
rclpy.init()
minimal_client = MinimalClientAsync()
response = minimal_client.send_request(int(sys.argv[1]), int(sys.argv[2]))
minimal_client.get_logger().info(
'Result of add_two_ints: for %d + %d = %d' %
(int(sys.argv[1]), int(sys.argv[2]), response.sum))
minimal_client.destroy_node()
rclpy.shutdown()
3.2 Add an entry point¶
Like the service node, you also have to add an entry point to be able to run the client node.
The entry_points field of your setup.py file should look like this:
4 Build and run¶
Itâs good practice to run rosdep in the root of your workspace (ros2_ws) to check for missing dependencies before building:
Navigate back to the root of your workspace, ros2_ws, and build your new package:
Open a new terminal, navigate to ros2_ws, and source the setup files:
Now run the service node:
The node will wait for the clientâs request.
Open another terminal and source the setup files from inside ros2_ws again. Start the client node, followed by any two integers separated by a space:
If you chose 2 and 3, for example, the client would receive a response like this:
Return to the terminal where your service node is running. You will see that it published log messages when it received the request:
Enter Ctrl+C
in each terminal to stop the nodes from spinning.
Summary¶
You created two nodes to request and respond to data over a service. You added their dependencies and executables to the package configuration files so that you could build and run them, allowing you to see a service/client system at work.
VIII Creating Custom Msg and Srv Files¶
Background¶
In previous tutorials you utilized message and service interfaces to learn about topics, services, and simple publisher/subscriber (C++/Python) and service/client (C++/Python) nodes. The interfaces you used were predefined in those cases.
While itâs good practice to use predefined interface definitions, you will probably need to define your own messages and services sometimes as well. This tutorial will introduce you to the simplest method of creating custom interface definitions.
Prerequisites¶
You should have a ROS 2 workspace.
This tutorial also uses the packages created in the publisher/subscriber (C++ and Python) and service/client (C++ and Python) tutorials to try out the new custom messages.
Task¶
1 Create a new package¶
For this tutorial you will be creating custom .msg
and .srv
files in their own package, and then utilizing them in a separate package. Both packages should be in the same workspace.
Since we will use the pub/sub and service/client packages created in earlier tutorials, make sure you are in the same workspace as those packages (ros2_ws/src
), and then run the following command to create a new package:
tutorial_interfaces
is the name of the new package. Note that it is, and can only be, a CMake
package, but this doesnât restrict in which type of packages you can use your messages and services. You can create your own custom interfaces in a CMake
package, and then use it in a C++ or Python node, which will be covered in the last section.
The .msg and .srv files are required to be placed in directories called msg
and srv
respectively. Create the directories in ros2_ws/src/tutorial_interfaces:
2 Create custom definitions¶
2.1 msg definition¶
In the tutorial_interfaces/msg
directory you just created, make a new file called Num.msg
with one line of code declaring its data structure:
This is a custom message that transfers a single 64-bit integer called num
.
Also in the tutorial_interfaces/msg
directory you just created, make a new file called Sphere.msg
with the following content:
2.2 srv definition¶
Back in the tutorial_interfaces/srv
directory you just created, make a new file called AddThreeInts.srv
with the following request and response structure:
This is your custom service that requests three integers named a, b, and c, and responds with an integer called sum.
3 CMakeLists.txt¶
To convert the interfaces you defined into language-specific code (like C++ and Python) so that they can be used in those languages, add the following lines to CMakeLists.txt:
find_package(geometry_msgs REQUIRED)
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/Num.msg"
"msg/Sphere.msg"
"srv/AddThreeInts.srv"
DEPENDENCIES geometry_msgs # Add packages that above messages depend on, in this case geometry_msgs for Sphere.msg
)
Note
The first argument (library name) in the rosidl_generate_interfaces must match ${PROJECT_NAME} (see https://github.com/ros2/rosidl/issues/441#issuecomment-591025515).
4 package.xml¶
Because the interfaces rely on rosidl_default_generators
for generating language-specific code, you need to declare a build tool dependency on it. rosidl_default_runtime
is a runtime or execution-stage dependency, needed to be able to use the interfaces later. The rosidl_interface_packages
is the name of the dependency group that your package, tutorial_interfaces
, should be associated with, declared using the
Add the following lines within the
<depend>geometry_msgs</depend>
<buildtool_depend>rosidl_default_generators</buildtool_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
5 Build the tutorial_interfaces package¶
Now that all the parts of your custom interfaces package are in place, you can build the package. In the root of your workspace (~/ros2_ws
), run the following command:
Now the interfaces will be discoverable by other ROS 2 packages.
6 Confirm msg and srv creation¶
In a new terminal, run the following command from within your workspace (ros2_ws) to source it:
Now you can confirm that your interface creation worked by using the ros2 interface show command:
You should see the following output:
You should see the following output:
and
You should see the following output:
7 Test the new interfaces¶
For this step you can use the packages you created in previous tutorials. A few simple modifications to the nodes, CMakeLists.txt and package.xml files will allow you to use your new interfaces.
7.1 Testing Num.msg with pub/subï¶
With a few modifications to the publisher/subscriber package created in a previous tutorial (C++ or Python), you can see Num.msg in action. Since youâll be changing the standard string msg to a numerical one, the output will be slightly different.
Publisher
#include <chrono>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/msg/num.hpp" // CHANGE
using namespace std::chrono_literals;
class MinimalPublisher : public rclcpp::Node
{
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
{
publisher_ = this->create_publisher<tutorial_interfaces::msg::Num>("topic", 10); // CHANGE
timer_ = this->create_wall_timer(
500ms, std::bind(&MinimalPublisher::timer_callback, this));
}
private:
void timer_callback()
{
auto message = tutorial_interfaces::msg::Num(); // CHANGE
message.num = this->count_++; // CHANGE
RCLCPP_INFO_STREAM(this->get_logger(), "Publishing: '" << message.num << "'"); // CHANGE
publisher_->publish(message);
}
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<tutorial_interfaces::msg::Num>::SharedPtr publisher_; // CHANGE
size_t count_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalPublisher>());
rclcpp::shutdown();
return 0;
}
import rclpy
from rclpy.node import Node
from tutorial_interfaces.msg import Num # CHANGE
class MinimalPublisher(Node):
def __init__(self):
super().__init__('minimal_publisher')
self.publisher_ = self.create_publisher(Num, 'topic', 10) # CHANGE
timer_period = 0.5
self.timer = self.create_timer(timer_period, self.timer_callback)
self.i = 0
def timer_callback(self):
msg = Num() # CHANGE
msg.num = self.i # CHANGE
self.publisher_.publish(msg)
self.get_logger().info('Publishing: "%d"' % msg.num) # CHANGE
self.i += 1
def main(args=None):
rclpy.init(args=args)
minimal_publisher = MinimalPublisher()
rclpy.spin(minimal_publisher)
minimal_publisher.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Subscriber
#include <functional>
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/msg/num.hpp" // CHANGE
using std::placeholders::_1;
class MinimalSubscriber : public rclcpp::Node
{
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
subscription_ = this->create_subscription<tutorial_interfaces::msg::Num>( // CHANGE
"topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
}
private:
void topic_callback(const tutorial_interfaces::msg::Num & msg) const // CHANGE
{
RCLCPP_INFO_STREAM(this->get_logger(), "I heard: '" << msg.num << "'"); // CHANGE
}
rclcpp::Subscription<tutorial_interfaces::msg::Num>::SharedPtr subscription_; // CHANGE
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalSubscriber>());
rclcpp::shutdown();
return 0;
}
import rclpy
from rclpy.node import Node
from tutorial_interfaces.msg import Num # CHANGE
class MinimalSubscriber(Node):
def __init__(self):
super().__init__('minimal_subscriber')
self.subscription = self.create_subscription(
Num, # CHANGE
'topic',
self.listener_callback,
10)
self.subscription
def listener_callback(self, msg):
self.get_logger().info('I heard: "%d"' % msg.num) # CHANGE
def main(args=None):
rclpy.init(args=args)
minimal_subscriber = MinimalSubscriber()
rclpy.spin(minimal_subscriber)
minimal_subscriber.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
CMakeLists.txt
Add the following lines (C++ only):
#...
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(tutorial_interfaces REQUIRED) # CHANGE
add_executable(talker src/publisher_member_function.cpp)
ament_target_dependencies(talker rclcpp tutorial_interfaces) # CHANGE
add_executable(listener src/subscriber_member_function.cpp)
ament_target_dependencies(listener rclcpp tutorial_interfaces) # CHANGE
install(TARGETS
talker
listener
DESTINATION lib/${PROJECT_NAME})
ament_package()
package.xml
Add the following lines:
After making the above edits and saving all the changes, build the package:
Then open two new terminals, source ros2_ws in each, and run:
Since Num.msg relays only an integer, the talker should only be publishing integer values, as opposed to the string it published previously:
[INFO] [minimal_publisher]: Publishing: '0'
[INFO] [minimal_publisher]: Publishing: '1'
[INFO] [minimal_publisher]: Publishing: '2'
7.2 Testing AddThreeInts.srv with service/clientï¶
With a few modifications to the service/client package created in a previous tutorial (C++ or Python), you can see AddThreeInts.srv in action. Since youâll be changing the original two integer request srv to a three integer request srv, the output will be slightly different.
Service
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/srv/add_three_ints.hpp" // CHANGE
#include <memory>
void add(const std::shared_ptr<tutorial_interfaces::srv::AddThreeInts::Request> request, // CHANGE
std::shared_ptr<tutorial_interfaces::srv::AddThreeInts::Response> response) // CHANGE
{
response->sum = request->a + request->b + request->c; // CHANGE
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld" " b: %ld" " c: %ld", // CHANGE
request->a, request->b, request->c); // CHANGE
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sending back response: [%ld]", (long int)response->sum);
}
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_three_ints_server"); // CHANGE
rclcpp::Service<tutorial_interfaces::srv::AddThreeInts>::SharedPtr service = // CHANGE
node->create_service<tutorial_interfaces::srv::AddThreeInts>("add_three_ints", &add); // CHANGE
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add three ints."); // CHANGE
rclcpp::spin(node);
rclcpp::shutdown();
}
from tutorial_interfaces.srv import AddThreeInts # CHANGE
import rclpy
from rclpy.node import Node
class MinimalService(Node):
def __init__(self):
super().__init__('minimal_service')
self.srv = self.create_service(AddThreeInts, 'add_three_ints', self.add_three_ints_callback) # CHANGE
def add_three_ints_callback(self, request, response):
response.sum = request.a + request.b + request.c # CHANGE
self.get_logger().info('Incoming request\na: %d b: %d c: %d' % (request.a, request.b, request.c)) # CHANGE
return response
def main(args=None):
rclpy.init(args=args)
minimal_service = MinimalService()
rclpy.spin(minimal_service)
rclpy.shutdown()
if __name__ == '__main__':
main()
Client
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/srv/add_three_ints.hpp" // CHANGE
#include <chrono>
#include <cstdlib>
#include <memory>
using namespace std::chrono_literals;
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
if (argc != 4) { // CHANGE
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "usage: add_three_ints_client X Y Z"); // CHANGE
return 1;
}
std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_three_ints_client"); // CHANGE
rclcpp::Client<tutorial_interfaces::srv::AddThreeInts>::SharedPtr client = // CHANGE
node->create_client<tutorial_interfaces::srv::AddThreeInts>("add_three_ints"); // CHANGE
auto request = std::make_shared<tutorial_interfaces::srv::AddThreeInts::Request>(); // CHANGE
request->a = atoll(argv[1]);
request->b = atoll(argv[2]);
request->c = atoll(argv[3]); // CHANGE
while (!client->wait_for_service(1s)) {
if (!rclcpp::ok()) {
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");
return 0;
}
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");
}
auto result = client->async_send_request(request);
// Wait for the result.
if (rclcpp::spin_until_future_complete(node, result) ==
rclcpp::FutureReturnCode::SUCCESS)
{
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Sum: %ld", result.get()->sum);
} else {
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Failed to call service add_three_ints"); // CHANGE
}
rclcpp::shutdown();
return 0;
}
from tutorial_interfaces.srv import AddThreeInts # CHANGE
import sys
import rclpy
from rclpy.node import Node
class MinimalClientAsync(Node):
def __init__(self):
super().__init__('minimal_client_async')
self.cli = self.create_client(AddThreeInts, 'add_three_ints') # CHANGE
while not self.cli.wait_for_service(timeout_sec=1.0):
self.get_logger().info('service not available, waiting again...')
self.req = AddThreeInts.Request() # CHANGE
def send_request(self):
self.req.a = int(sys.argv[1])
self.req.b = int(sys.argv[2])
self.req.c = int(sys.argv[3]) # CHANGE
self.future = self.cli.call_async(self.req)
def main(args=None):
rclpy.init(args=args)
minimal_client = MinimalClientAsync()
minimal_client.send_request()
while rclpy.ok():
rclpy.spin_once(minimal_client)
if minimal_client.future.done():
try:
response = minimal_client.future.result()
except Exception as e:
minimal_client.get_logger().info(
'Service call failed %r' % (e,))
else:
minimal_client.get_logger().info(
'Result of add_three_ints: for %d + %d + %d = %d' % # CHANGE
(minimal_client.req.a, minimal_client.req.b, minimal_client.req.c, response.sum)) # CHANGE
break
minimal_client.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
CMakeLists.txt
Add the following lines (C++ only):
#...
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(tutorial_interfaces REQUIRED) # CHANGE
add_executable(server src/add_two_ints_server.cpp)
ament_target_dependencies(server
rclcpp tutorial_interfaces) # CHANGE
add_executable(client src/add_two_ints_client.cpp)
ament_target_dependencies(client
rclcpp tutorial_interfaces) # CHANGE
install(TARGETS
server
client
DESTINATION lib/${PROJECT_NAME})
ament_package()
Add the following lines:
After making the above edits and saving all the changes, build the package:
Then open two new terminals, source ros2_ws in each, and run:
Summary¶
In this tutorial, you learned how to create custom interfaces in their own package and how to utilize those interfaces in other packages.
This tutorial only scratches the surface about defining custom interfaces. You can learn more about it in About ROS 2 interfaces.