03 ROS Client Libraries
03 ROS Client Libraries
Goal: Create a workspace and learn how to set up an overlay for development and testing.
Tutorial level: Beginner
Time: 20 minutes
Contents
Background
Prerequisites
Tasks
1 Source ROS 2 environment
2 Create a new directory
3 Clone a sample repo
4 Resolve dependencies
5 Build the workspace with colcon
6 Source the overlay
7 Modify the overlay
o Summary
o Next steps
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:
LinuxmacOSWindows
source /opt/ros/galactic/setup.bash
Consult the installation guide you followed if these commands don’t work for you.
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 dev_ws , for “development workspace”:
LinuxmacOSWindows
mkdir -p ~/dev_ws/src
cd ~/dev_ws/src
Another best practice is to put any packages in your workspace into the src directory. The above
code creates a src directory inside dev_ws and then navigates into it.
3 Clone a sample repo
Ensure you’re still in the dev_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.
The existing packages you will use are from the ros_tutorials repository (repo). If you went
through the “Beginner: CLI Tools” tutorials, you’ll be familiar with turtlesim , one of the packages in
this repo.
You can see the repo on GitHub.
Notice the “Branch” drop down list to the left above the directories list. When you clone this repo,
add the -b argument followed by the branch that corresponds with your ROS 2 distro.
In the dev_ws/src directory, run the following command for the distro you’re using:
git clone https://github.com/ros/ros_tutorials.git -b galactic-devel
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.
Now you have populated your workspace with a sample package, but it isn’t a fully-functional
workspace yet. You need to resolve dependencies and build the workspace first.
4 Resolve dependencies
Before building the workspace, you need to resolve 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 because of missing dependencies.
From the root of your workspace ( dev_ws ), run the following command:
LinuxmacOSWindows
# cd if you're still in the ``src`` directory with the ``ros_tutorials`` clone
cd ..
rosdep install -i --from-path src --rosdistro galactic -y
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:
#All required rosdeps installed successfully
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 ( dev_ws ), you can now build your packages using the command:
LinuxmacOSWindows
colcon build
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
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:
LinuxmacOSWindows
source /opt/ros/galactic/setup.bash
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 dev_ws overlay’s local_setup , like
you just did, is the same as just sourcing dev_ws ’s setup , because that includes the environment of
the underlay it was created in.
Now you can run the turtlesim package from the overlay:
ros2 run turtlesim turtlesim_node
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 ~/dev_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 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:
ros2 run turtlesim turtlesim_node
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 dev_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:
ros2 run turtlesim turtlesim_node
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.
Next steps
Now that you understand the details behind creating, building and sourcing your own workspace,
you can learn how to create your own packages.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Creating your first ROS 2 package
Goal: Create a new package using either CMake or Python, and run its executable.
Tutorial level: Beginner
Time: 15 minutes
Contents
Background
1 What is a ROS 2 package?
2 What makes up a ROS 2 package?
3 Packages in a workspace
o Prerequisites
o Tasks
1 Create a package
2 Build a package
3 Source the setup file
4 Use the package
5 Examine package contents
6 Customize package.xml
o Summary
o Next steps
Background
1 What is a ROS 2 package?
A package can be considered a container 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:
CMakePython
package.xml file containing meta information about the package
CMakeLists.txt file that describes how to build the code within the package
The simplest possible package may have a file structure that looks like:
CMakePython
my_package/
CMakeLists.txt
package.xml
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/
package_1/
CMakeLists.txt
package.xml
package_2/
setup.py
package.xml
resource/package_2
...
package_n/
CMakeLists.txt
package.xml
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, dev_ws , for your new package.`
Make sure you are in the src folder before running the package creation command.
LinuxmacOSWindows
cd ~/dev_ws/src
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:
CMakePython
ros2 pkg create --build-type ament_cmake --node-name my_node my_package
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:
CMakePython
going to create a new package
package name: my_package
destination directory: /home/user/dev_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
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:
LinuxmacOSWindows
cd ~/dev_ws
Recall from the last tutorial that you also have the ros_tutorials packages in your dev_ws . You
might’ve 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:
colcon build --packages-select my_package
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:
ros2 run my_package my_node
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 dev_ws/src/my_package , open package.xml using your preferred text editor:
CMakePython
<?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="[email protected]">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>
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:
<description>Beginner client libraries tutorials practice package</description>
Then update the 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 use Apache License 2.0 :
<license>Apache License 2.0</license>
Your terminal will return a message verifying the creation of your package cpp_pubsub and all its
necessary files and folders.
Navigate into dev_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:
LinuxmacOSWindows
wget -O publisher_member_function.cpp
https://raw.githubusercontent.com/ros2/examples/master/rclcpp/topics/minimal_publisher/member_function.cp
p
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"
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_;
};
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
The next line creates the node class MinimalPublisher by inheriting from rclcpp::Node . Every this in
the code is referring to the node.
class MinimalPublisher : public rclcpp::Node
The public constructor names 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));
}
The 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);
}
Following the 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;
}
This declares the package needs rclcpp and std_msgs when its code is 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:
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
After that, add the executable and name it 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:
install(TARGETS
talker
DESTINATION lib/${PROJECT_NAME})
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()
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 dev_ws/src/cpp_pubsub/src to create the next node. Enter the following code in your
terminal:
LinuxmacOSWindows
wget -O subscriber_member_function.cpp
https://raw.githubusercontent.com/ros2/examples/master/rclcpp/topics/minimal_subscriber/member_function.c
pp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using std::placeholders::_1;
private:
void topic_callback(const std_msgs::msg::String::SharedPtr msg) const
{
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str());
}
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};
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::SharedPtr 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 for use.
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 ( dev_ws ) to check for missing
dependencies before building:
LinuxmacOSWindows
rosdep install -i --from-path src --rosdistro galactic -y
Still in the root of your workspace, dev_ws , build your new package:
LinuxmacOSWindows
colcon build --packages-select cpp_pubsub
Open a new terminal, navigate to dev_ws , and source the setup files:
LinuxmacOSWindows
. install/setup.bash
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 dev_ws again, and then start the listener
node:
ros2 run cpp_pubsub listener
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"
Enter 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.
Next steps
Next you’ll create another simple ROS 2 package using the service/client model. Again, you can
choose to write it in either C++ or Python.
Related content
There are several ways you could write a publisher and subscriber in C++; check out
the minimal_publisher and minimal_subscriber packages in the ros2/examples repo.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Writing a simple service and client (C++)
Goal: Create and run service and client nodes using C++.
Tutorial level: Beginner
Time: 20 minutes
Contents
Background
Prerequisites
Tasks
1 Create a package
2 Write the service node
3 Write the client node
4 Build and run
o Summary
o Next steps
o Related content
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 dev_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 dev_ws/src and create a new package:
ros2 pkg create --build-type ament_cmake 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:
int64 a
int64 b
---
int64 sum
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="[email protected]">Your Name</maintainer>
<license>Apache License 2.0</license>
#include <memory>
rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr service =
node->create_service<example_interfaces::srv::AddTwoInts>("add_two_ints", &add);
rclcpp::spin(node);
rclcpp::shutdown();
}
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);
So ros2 run can find the executable, add the following lines to the end of the file, right
before ament_package() :
install(TARGETS
server
DESTINATION lib/${PROJECT_NAME})
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 dev_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>
if (argc != 3) {
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "usage: add_two_ints_client X Y");
return 1;
}
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...");
}
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.
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");
If the client is canceled (e.g. by you entering Ctrl+C into the terminal), it will return an error log
message stating it was interrupted.
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");
return 0;
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()
Open a new terminal, navigate to dev_ws , and source the setup files:
LinuxmacOSWindows
. install/setup.bash
The terminal should return the following message, and then wait:
[INFO] [rclcpp]: Ready to add two ints.
Open another terminal, source the setup files from inside dev_ws again. Start the client node,
followed by any two integers separated by a space:
ros2 run cpp_srvcli client 2 3
If you chose 2 and 3 , for example, the client would receive a response like this:
[INFO] [rclcpp]: Sum: 5
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:
[INFO] [rclcpp]: Incoming request
a: 2 b: 3
[INFO] [rclcpp]: sending back response: [5]
Enter Ctrl+C in the server terminal to stop the node 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
Next steps
In the last few tutorials you’ve been utilizing interfaces to pass data across topics and services.
Next, you’ll learn how to create custom interfaces.
Related content
There are several ways you could write a service and client in C++; check out
the minimal_service and minimal_client packages in the ros2/examples repo.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
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:
int64 a
int64 b
---
int64 sum
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 .
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="[email protected]">Your Name</maintainer>
<license>Apache License 2.0</license>
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)
return response
def main():
rclpy.init()
minimal_service = MinimalService()
rclpy.spin(minimal_service)
rclpy.shutdown()
if __name__ == '__main__':
main()
import rclpy
from rclpy.node import Node
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)
The definition of the service callback receives the request data, sums it, and returns the sum as a
response.
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 dev_ws/src/py_srvcli directory).
Add the following line between the 'console_scripts': brackets:
'service = py_srvcli.service_member_function:main',
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):
self.req.a = int(sys.argv[1])
self.req.b = int(sys.argv[2])
self.future = self.cli.call_async(self.req)
def main():
rclpy.init()
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_two_ints: for %d + %d = %d' %
(minimal_client.req.a, minimal_client.req.b, response.sum))
break
minimal_client.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Navigate back to the root of your workspace, dev_ws , and build your new package:
colcon build --packages-select py_srvcli
Open a new terminal, navigate to dev_ws , and source the setup files:
LinuxmacOSWindows
. install/setup.bash
If you chose 2 and 3 , for example, the client would receive a response like this:
[INFO] [minimal_client_async]: Result of add_two_ints: for 2 + 3 = 5
Return to the terminal where your service node is running. You will see that it published log
messages when it received the request:
[INFO] [minimal_service]: Incoming request
a: 2 b: 3
Enter Ctrl+C in the server terminal to stop the node 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.
Next steps
In the last few tutorials you’ve been utilizing interfaces to pass data across topics and services.
Next, you’ll learn how to create custom interfaces.
Related content
There are several ways you could write a service and client in Python; check out
the minimal_client and minimal_service packages in the ros2/examples repo.
In this tutorial, you used the call_async() API in your client node to call the service. There is
another service call API available for Python called synchronous calls. We do not recommend
using synchronous calls, but if you’d like to learn more about them, read the guide
to Synchronous vs. asynchronous clients.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
4 package.xml
5 Build the tutorial_interfaces package
6 Confirm msg and srv creation
7 Test the new interfaces
o Summary
o Next steps
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.
Tasks
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 ( dev_ws/src ), and then run the following
command to create a new package:
ros2 pkg create --build-type ament_cmake tutorial_interfaces
tutorial_interfaces is the name of the new package. Note that it is a CMake package; there
currently isn’t a way to generate a .msg or .srv file in a pure Python package. You can create a
custom interface in a CMake package, and then use it in a Python node, which will be covered in
the last section.
It is good practice to keep .msg and .srv files in their own directories within a package. Create the
directories in dev_ws/src/tutorial_interfaces :
mkdir msg
mkdir srv
This is your custom message that transfers a single 64-bit integer called num .
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:
int64 a
int64 b
int64 c
---
int64 sum
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(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/Num.msg"
"srv/AddThreeInts.srv"
)
4 package.xml
Because the interfaces rely on rosidl_default_generators for generating language-specific code,
you need to declare a dependency on it. Add the following lines to package.xml
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
Now you can confirm that your interface creation worked by using
the ros2 interface show command:
ros2 interface show tutorial_interfaces/msg/Num
should return:
int64 num
And
ros2 interface show tutorial_interfaces/srv/AddThreeInts
should return:
int64 a
int64 b
int64 c
---
int64 sum
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/msg/num.hpp" // CHANGE
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_;
};
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/msg/num.hpp" // CHANGE
using std::placeholders::_1;
private:
void topic_callback(const tutorial_interfaces::msg::Num::SharedPtr msg) const // CHANGE
{
RCLCPP_INFO_STREAM(this->get_logger(), "I heard: '" << msg->num << "'"); // CHANGE
}
rclcpp::Subscription<tutorial_interfaces::msg::Num>::SharedPtr subscription_; // CHANGE
};
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 line:
C++Python
<depend>tutorial_interfaces</depend>
After making the above edits and saving all the changes, build the package:
C++Python
colcon build --packages-select cpp_pubsub
On Windows:
colcon build --merge-install --packages-select cpp_pubsub
Then open two new terminals, source dev_ws in each, and run:
C++Python
ros2 run cpp_pubsub talker
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'
#include <memory>
rclcpp::spin(node);
rclcpp::shutdown();
}
Client:
C++Python
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/srv/add_three_ints.hpp" // CHANGE
#include <chrono>
#include <cstdlib>
#include <memory>
if (argc != 4) { // CHANGE
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "usage: add_three_ints_client X Y Z"); // CHANGE
return 1;
}
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...");
}
rclcpp::shutdown();
return 0;
}
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()
package.xml:
Add the following line:
C++Python
<depend>tutorial_interfaces</depend>
After making the above edits and saving all the changes, build the package:
C++Python
colcon build --packages-select cpp_srvcli
On Windows:
colcon build --merge-install --packages-select cpp_srvcli
Then open two new terminals, source dev_ws in each, and run:
C++Python
ros2 run cpp_srvcli server
Summary
In this tutorial, you learned how to create custom interfaces in their own package and how to utilize
those interfaces from within other packages.
This is a simple method of interface creation and utilization. You can learn more about
interfaces here.
Next steps
The next tutorial covers more ways to use interfaces in ROS 2.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
string first_name
string last_name
bool gender
uint8 age
string address
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
By adding the .msg files manually, we make sure that CMake knows when it has to reconfigure the
project after you add other .msg files.
Generate the messages:
rosidl_generate_interfaces(${PROJECT_NAME}
${msg_files}
)
Now you’re ready to generate source files from your msg definition. We’ll skip the compile step for
now as we do it all together below in step 4.
2.2 (Extra) Set multiple interfaces
Note
You can use set to neatly list all of your interfaces:
set(msg_files
"msg/Message1.msg"
"msg/Message2.msg"
# etc
)
set(srv_files
"srv/Service1.srv"
"srv/Service2.srv"
# etc
)
#include "rclcpp/rclcpp.hpp"
#include "more_interfaces/msg/address_book.hpp"
message.first_name = "John";
message.last_name = "Doe";
message.age = 30;
message.gender = message.MALE;
message.address = "unknown";
this->address_book_publisher_->publish(message);
};
timer_ = this->create_wall_timer(1s, publish_msg);
}
private:
rclcpp::Publisher<more_interfaces::msg::AddressBook>::SharedPtr address_book_publisher_;
rclcpp::TimerBase::SharedPtr timer_;
};
return 0;
}
this->address_book_publisher_->publish(message);
add_executable(publish_address_book
src/publish_address_book.cpp
)
ament_target_dependencies(publish_address_book
"rclcpp"
)
install(TARGETS publish_address_book
DESTINATION lib/${PROJECT_NAME})
This finds the relevant generated C++ code from AddressBook.msg and allows your target to link
against it.
You may have noticed that this step was not necessary when the interfaces being used were from
a package that was built separately. This CMake code is only required when you want to use
interfaces in the same package as the one in which they are used.
4 Try it out
Return to the root of the workspace to build the package:
LinuxmacOSWindows
cd ~/dev_ws
colcon build --packages-up-to more_interfaces
You should see the publisher relaying the msg you defined, including the values you set
in publish_address_book.cpp .
To confirm the message is being published on the address_book topic, open another terminal,
source the workspace, and call topic echo :
LinuxmacOSWindows
. install/setup.bash
ros2 topic echo /address_book
We won’t create a subscriber in this tutorial, but you can try to write one yourself for practice
(use Writing a simple publisher and subscriber (C++) to help).
5 (Extra) Use an existing interface definition
Note
You can use an existing interface definition in a new interface definition. For example, let’s say
there is a message named Contact.msg that belongs to an existing ROS 2 package
named rosidl_tutorials_msgs . Assume that its definition is identical to our custom-
made AddressBook.msg interface from earlier.
In that case you could have defined AddressBook.msg (an interface in the package with your nodes)
as type Contact (an interface in a separate package). You could even define AddressBook.msg as
an array of type Contact , like so:
rosidl_tutorials_msgs/Contact[] address_book
<exec_depend>rosidl_tutorials_msgs</exec_depend>
rosidl_generate_interfaces(${PROJECT_NAME}
${msg_files}
DEPENDENCIES rosidl_tutorials_msgs
)
You would also need to include the header of Contact.msg in you publisher node in order to be able
to add contacts to your address_book .
#include "rosidl_tutorials_msgs/msg/contact.hpp"
address_book_publisher_->publish(*msg);
};
Building and running these changes would show the msg defined as expected, as well as the
array of msgs defined above.
Summary
In this tutorial, you tried out different field types for defining interfaces, then built an interface in the
same package where it’s being used.
You also learned how to use another interface as a field type, as well as
the package.xml , CMakeLists.txt , and #include statements necessary for utilizing that feature.
Next steps
Next you will create a simple ROS 2 package with a custom parameter that you will learn to set
from a launch file. Again, you can choose to write it in either C++ or Python.
Related content
There are several design articles on ROS 2 interfaces and the IDL (interface definition language).
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Your terminal will return a message verifying the creation of your package cpp_parameters and all its
necessary files and folders.
The --dependencies argument will automatically add the necessary dependency lines
to package.xml and CMakeLists.txt .
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++ parameter tutorial</description>
<maintainer email="[email protected]">Your Name</maintainer>
<license>Apache License 2.0</license>
The first line of our respond function gets the parameter my_parameter from the node, and stores it
in parameter_string_ . The RCLCPP_INFO function ensures the message is logged.
void respond()
{
this->get_parameter("my_parameter", parameter_string_);
RCLCPP_INFO(this->get_logger(), "Hello %s", parameter_string_.c_str());
}
Following our ParametersClass is our main . Here ROS 2 is initialized, and rclcpp::spin starts
processing data from the node.
int main(int argc, char** argv)
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<ParametersClass>());
rclcpp::shutdown();
return 0;
}
install(TARGETS
parameter_node
DESTINATION lib/${PROJECT_NAME}
)
Navigate back to the root of your workspace, dev_ws , and build your new package:
LinuxmacOSWindows
colcon build --packages-select cpp_parameters
Open a new terminal, navigate to dev_ws , and source the setup files:
LinuxmacOSWindows
. install/setup.bash
Now you can see the default value of your parameter, but you want to be able to set it yourself.
There are two ways to accomplish this.
3.1 Change via the console
This part will use the knowledge you have gained from the tutorial about parameters and apply it
to the node you have just created.
Make sure the node is running:
ros2 run cpp_parameters parameter_node
Open another terminal, source the setup files from inside dev_ws again, and enter the following
line:
ros2 param list
There you will see the custom parameter my_parameter . To change it simply run the following line in
the console:
ros2 param set /parameter_node my_parameter earth
You know it went well if you get the output Set parameter successful . If you look at the other
terminal, you should see the output change to [INFO] [parameter_node]: Hello earth
3.2 Change via a launch file
You can also set the parameter in a launch file, but first you will need to add the launch directory.
Inside the dev_ws/src/cpp_parameters/ directory, create a new directory called launch . In there,
create a new file called cpp_parameters_launch.py
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package="cpp_parameters",
executable="parameter_node",
name="custom_parameter_node",
output="screen",
emulate_tty=True,
parameters=[
{"my_parameter": "earth"}
]
)
])
Here you can see that we set my_parameter to earth when we launch our node parameter_node . By
adding the two lines below, we ensure our output is printed in our console.
output="screen",
emulate_tty=True,
Now open the CMakeLists.txt file. Below the lines you added earlier, add the following lines of
code.
install(
DIRECTORY launch
DESTINATION share/${PROJECT_NAME}
)
Open a console and navigate to the root of your workspace, dev_ws , and build your new package:
LinuxmacOSWindows
colcon build --packages-select cpp_parameters
Summary
You created a node with a custom parameter, that can be set either from a launch file or the
command line. You added the dependencies, executables, and a launch file to the package
configuration files so that you could build and run them, and see the parameter in action.
Next steps
Now that you have some packages and ROS 2 systems of your own, the next tutorial will show
you how to examine issues in your environment and systems in case you have problems.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
class MinimalParam(rclpy.node.Node):
def __init__(self):
super().__init__('minimal_param_node')
timer_period = 2 # seconds
self.timer = self.create_timer(timer_period, self.timer_callback)
self.declare_parameter('my_parameter', 'world')
def timer_callback(self):
my_param = self.get_parameter('my_parameter').get_parameter_value().string_value
my_new_param = rclpy.parameter.Parameter(
'my_parameter',
rclpy.Parameter.Type.STRING,
'world'
)
all_new_parameters = [my_new_param]
self.set_parameters(all_new_parameters)
def main():
rclpy.init()
node = MinimalParam()
rclpy.spin(node)
if __name__ == '__main__':
main()
The next piece of code creates the class and the constructor. timer is initialized (with timer_period
set as 2 seconds), which causes the timer_callback function to be executed once every two
seconds. The line self.declare_parameter('my_parameter', 'world') of the constructor creates a
parameter with the name my_parameter and a default value of world .
class MinimalParam(rclpy.node.Node):
def __init__(self):
super().__init__('minimal_param_node')
timer_period = 2 # seconds
self.timer = self.create_timer(timer_period, self.timer_callback)
self.declare_parameter('my_parameter', 'world')
The first line of our timer_callback function gets the parameter my_parameter from the node, and
stores it in my_param . Next,the get_logger function ensures the message is logged. Then, we set the
parameter ‘my_parameter’ back to the default string value ‘world’.
def timer_callback(self):
my_param = self.get_parameter('my_parameter').get_parameter_value().string_value
my_new_param = rclpy.parameter.Parameter(
'my_parameter',
rclpy.Parameter.Type.STRING,
'world'
)
all_new_parameters = [my_new_param]
self.set_parameters(all_new_parameters)
Following the timer_callback is the main function where ROS 2 is initialized. Then an instance of
the class MinimalParam named node is defined. Finally, rclpy.spin starts processing data from the
node.
def main():
rclpy.init()
node = MinimalParam()
rclpy.spin(node)
if __name__ == '__main__':
main()
class MinimalParam(rclpy.node.Node):
def __init__(self):
super().__init__('minimal_param_node')
timer_period = 2 # seconds
self.timer = self.create_timer(timer_period, self.timer_callback)
The rest of the code remains the same. Once you run the node, you can then
run ros2 param describe /minimal_param_node my_parameter to see the type and description.
2.2 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='[email protected]',
description='Python parameter tutorial',
license='Apache License 2.0',
Add the following line within the console_scripts brackets of the entry_points field:
entry_points={
'console_scripts': [
'param_talker = python_parameters.python_parameters_node:main',
],
},
Navigate back to the root of your workspace, dev_ws , and build your new package:
LinuxmacOSWindows
colcon build --packages-select python_parameters
Open a new terminal, navigate to dev_ws , and source the setup files:
LinuxmacOSWindows
. install/setup.bash
Except the first message where the parameter had a default value (an empty string), the terminal
should return the following message every 2 seconds:
[INFO] [parameter_node]: Hello world!
Open another terminal, source the setup files from inside dev_ws again, and enter the following
line:
ros2 param list
There you will see the custom parameter my_parameter . To change it simply run the following line in
the console:
ros2 param set /minimal_param_node my_parameter earth
You know it went well if you get the output Set parameter successful . If you look at the other
terminal, you should see the output change to [INFO] [minimal_param_node]: Hello earth!
Since the Python talker then set the parameter back to world , further outputs
show [INFO] [minimal_param_node]: Hello world!
3.2 Change via a launch file
You can also set parameters in a launch file, but first you will need to add a launch directory.
Inside the dev_ws/src/python_parameters/ directory, create a new directory called launch . In there,
create a new file called python_parameters_launch.py
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='python_parameters',
executable='param_talker',
name='custom_parameter_node',
output='screen',
emulate_tty=True,
parameters=[
{'my_parameter': 'earth'}
]
)
])
Here you can see that we set my_parameter to earth when we launch our node parameter_node . By
adding the two lines below, we ensure our output is printed in our console.
output="screen",
emulate_tty=True,
Now open the setup.py file. Add the import statements to the top of the file, and the other new
statement to the data_files parameter to include all launch files:
import os
from glob import glob
# ...
setup(
# ...
data_files=[
# ...
(os.path.join('share', package_name), glob('launch/*_launch.py')),
]
)
Open a console and navigate to the root of your workspace, dev_ws , and build your new package:
LinuxmacOSWindows
colcon build --packages-select python_parameters
Now run the node using the launch file we have just created:
ros2 launch python_parameters python_parameters_launch.py
Summary
You created a node with a custom parameter, that can be set either from the launch file or the
command line. You wrote the code of a parameter talker: a Python node that declares, and then
loops getting and setting a string parameter. You added the entry point so that you could build and
run it, and used ros2 param to interact with the parameter talker.
Next steps
Now that you have some packages and ROS 2 systems of your own, the next tutorial will show
you how to examine issues in your environment and systems in case you have problems.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
This will conduct checks over all your setup modules and return warnings and errors.
If your ROS 2 setup is in perfect shape, you’ll see a message similar to this:
All <n> checks passed
However, it’s not unusual to have a few warnings returned. A UserWarning doesn’t mean your setup
is unusable; it’s more likely just an indication that something is configured in a way that’s not ideal.
If you do receive a warning, it will look something like this:
<path>: <line>: UserWarning: <message>
For example, ros2doctor will find this warning if you’re using an unstable ROS 2 distribution:
UserWarning: Distribution <distro> is not fully supported or tested. To get more consistent features,
download a stable version at https://index.ros.org/doc/ros2/Installation/
If ros2doctor only finds warnings in your system, you will still receive
the All <n> checks passed message.
Most checks are categorized as warnings as opposed to errors. It’s mostly up to you, the user, to
determine the importance of the feedback ros2doctor returns. If it does find a rare error in your
setup, indicated by UserWarning: ERROR: , the check is considered failed.
You will see a message similar to this following the list of issue feedback:
1/3 checks failed
An error indicates the system is missing important settings or functions that are crucial to ROS 2.
Errors should be addressed to ensure the system functions properly.
2 Check a system
You can also examine a running ROS 2 system to identify possible causes for issues. To
see ros2doctor working on a running system, let’s run Turtlesim, which has nodes actively
communicating with each other.
Start up the system by opening a new terminal, sourcing ROS 2, and entering the command:
ros2 run turtlesim turtlesim_node
Open another terminal and source ROS 2 to run the teleop controls:
ros2 run turtlesim turtle_teleop_key
Now run ros2doctor again in its own terminal. You will see the warnings and errors you had the
last time you ran ros2doctor on your setup, if you had any. Following those will be a couple new
warnings relating to the system itself:
UserWarning: Publisher without subscriber detected on /turtle1/color_sensor.
UserWarning: Publisher without subscriber detected on /turtle1/pose.
It seems that the /turtlesim node publishes data to two topics that aren’t being subscribed to,
and ros2doctor thinks this could possibly lead to issues.
If you run commands to echo the /color_sensor and /pose topics, those warnings will disappear
because the publishers will have subscribers.
You can try this by opening two new terminals while turtlesim is still running, sourcing ROS 2 in
each, and running each of the following commands in their own terminal:
ros2 topic echo /turtle1/color_sensor
Then run ros2doctor in its terminal again. The publisher without subscriber warnings will be gone.
(Make sure to enter Ctrl+C in the terminals where you ran echo ).
Now try exiting either the turtlesim window or quitting the teleop and running ros2doctor again.
You’ll see more warnings indicating publisher without subscriber or subscriber without publisher for
different topics, now that one node in the system isn’t available.
In a complex system with many nodes, ros2doctor would be invaluable for identifying possible
reasons for communication issues.
3 Get a full report
While ros2doctor will let you know warnings about your network, system, etc., running it with
the --report argument will give you much more detail to help you analyze issues.
You might want to use --report if you get a warning about your network setup and want to find out
exactly what part of your configuration is causing the warning.
It’s also very helpful when you need to open a support ticket to get help with ROS 2. You can copy
and paste the relevant parts of your report into the ticket so the people helping you can better
understand your environment and provide better assistance.
To get a full report, enter the following command in the terminal:
ros2 doctor --report
PLATFORM INFORMATION
...
RMW MIDDLEWARE
...
ROS 2 INFORMATION
...
TOPIC LIST
...
You can crosscheck the information here against the warnings you get from running ros2 doctor .
For example, if ros2doctor returned the warning (mentioned earlier) that your distribution is “not
fully supported or tested”, you might take a look at the ROS 2 INFORMATION section of the report:
distribution name : <distro>
distribution type : ros2
distribution status : prerelease
release platforms : {'<platform>': ['<version>']}
Here you can see the distribution status is prerelease , which explains why it’s not fully supported.
Summary
ros2doctor will inform you of problems in your ROS 2 setup and running systems. You can get a
deeper look at information behind those warnings by using the --report argument.
Keep in mind, ros2doctor is not a debug tool; it won’t help with errors in your code or on the
implementation side of your system.
Related content
ros2doctor’s README will tell you more about different arguments. You might want to take a look
around the ros2doctor repo as well, since it’s fairly beginner friendly and a great place to get
started with contributing.
Next steps
You’ve completed the beginner level tutorials!
PreviousNext
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Tasks
In this tutorial, you will create a two new packages, one that defines the base class, and the other
that provides the plugins. The base class will define a generic polygon class, and then our plugins
will define specific shapes.
1 Create the Base Class Package
Create a new empty package in your dev_ws/src folder with the following terminal command.
ros2 pkg create --build-type ament_cmake polygon_base --dependencies pluginlib --node-name area_node
namespace polygon_base
{
class RegularPolygon
{
public:
virtual void initialize(double side_length) = 0;
virtual double area() = 0;
virtual ~RegularPolygon(){}
protected:
RegularPolygon(){}
};
} // namespace polygon_base
#endif // POLYGON_BASE_REGULAR_POLYGON_HPP
This code above should be pretty self explanatory… we’re creating an abstract class
called RegularPolygon . One thing to notice is the presence of the initialize method. With pluginlib , a
constructor without parameters is required for classes so, if any parameters are required, we use
the initialize method to initialize the object.
We need to make this header available to other classes, so
open dev_ws/src/polygon_base/CMakeLists.txt for editing. Add the following lines after
the ament_target_dependencies command.
install(
DIRECTORY include/
DESTINATION include
)
namespace polygon_plugins
{
class Square : public polygon_base::RegularPolygon
{
public:
void initialize(double side_length) override
{
side_length_ = side_length;
}
protected:
double side_length_;
};
double getHeight()
{
return sqrt((side_length_ * side_length_) - ((side_length_ / 2) * (side_length_ / 2)));
}
protected:
double side_length_;
};
}
#include <pluginlib/class_list_macros.hpp>
PLUGINLIB_EXPORT_CLASS(polygon_plugins::Square, polygon_base::RegularPolygon)
PLUGINLIB_EXPORT_CLASS(polygon_plugins::Triangle, polygon_base::RegularPolygon)
The implementation of the Square and Triangle classes should be fairly straightforward: save the
side length, and use it to calculate the area. The only piece that is pluginlib specific is the last
three lines, which invokes some magical macros that register the classes as actual plugins. Let’s
go through the arguments to the PLUGINLIB_EXPORT_CLASS macro:
1. The fully-qualified type of the plugin class, in this case, polygon_plugins::Square .
2. The fully-qualified type of the base class, in this case, polygon_base::RegularPolygon .
2.2 Plugin Declaration XML
The steps above make it so that instances of our plugins can be created once the library they exist
in is loaded, but the plugin loader still needs a way to find that library and to know what to
reference within that library. To this end, we’ll also create an XML file that, along with a special
export line in the package manifest, makes all the necessary information about our plugins
available to the ROS toolchain.
Create dev_ws/src/polygon_plugins/plugins.xml with the following code:
<library path="polygon_plugins">
<class type="polygon_plugins::Square" base_class_type="polygon_base::RegularPolygon">
<description>This is a square plugin.</description>
</class>
<class type="polygon_plugins::Triangle" base_class_type="polygon_base::RegularPolygon">
<description>This is a triangle plugin.</description>
</class>
</library>
: The fully qualified base class type for the plugin. For us,
base_class
that’s polygon_base::RegularPolygon .
description : A description of the plugin and what it does.
name : There used to be a name attribute, but it is no longer required.
2.3 CMake Plugin Declaration
The last step is to export your plugins via CMakeLists.txt . This is a change from ROS 1, where the
exporting was done via package.xml . Add the following block to
your dev_ws/src/polygon_plugins/CMakeLists.txt after the line reading find_package(pluginlib REQUIRED)
add_library(polygon_plugins src/polygon_plugins.cpp)
target_include_directories(polygon_plugins PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>)
ament_target_dependencies(
polygon_plugins
polygon_base
pluginlib
)
pluginlib_export_plugin_description_file(polygon_base plugins.xml)
install(
TARGETS polygon_plugins
EXPORT export_${PROJECT_NAME}
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
pluginlib::ClassLoader<polygon_base::RegularPolygon> poly_loader("polygon_base",
"polygon_base::RegularPolygon");
try
{
std::shared_ptr<polygon_base::RegularPolygon> triangle =
poly_loader.createSharedInstance("polygon_plugins::Triangle");
triangle->initialize(10.0);
std::shared_ptr<polygon_base::RegularPolygon> square =
poly_loader.createSharedInstance("polygon_plugins::Square");
square->initialize(10.0);
return 0;
}
The ClassLoader is the key class to understand, defined in the class_loader.hpp header.
It is templated with the base class, i.e. polygon_base::RegularPolygon
The first argument is a string for the package name of the base class, i.e. polygon_base
The second argument is a string with the fully qualified base class type for the plugin,
i.e. polygon_base::RegularPolygon
There are a number of ways to instantiate an instance of the class. In this example, we’re using
shared pointers. We just need to call createSharedInstance with the fully-qualified type of the plugin
class, in this case, polygon_plugins::Square .
Important note: the polygon_base package in which this node is defined does NOT depend on
the polygon_plugins class. The plugins will be loaded dynamically without any dependency needing
to be declared. Furthermore, we’re instantiating the classes with hardcoded plugin names, but you
can also do so dynamically with parameters, etc.
4 Build and run
Navigate back to the root of your workspace, dev_ws , and build your new packages:
colcon build --packages-select polygon_base polygon_plugins
It should print
Triangle area: 43.30
Square area: 100.00
Summary
Congratulations! You’ve just written and used your first plugins.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%