Creating C++ Nodes (for ament_cmake)
The C++ binary block for ament_cmake
TL;DR
When adding a new Node in an existing CMakeLists.txt
, you might benefit from using the following template.
Remember to:
Add ALL dependencies (including ROS2 ones) with
find_package
, if applicable.# find dependencies find_package(ament_cmake REQUIRED) find_package(rclcpp REQUIRED)
Change
print_forever_node
to the name of your Node.Add all source files to
add_executable
.Add all ROS2 dependencies of this binary to
ament_target_dependencies
.Add any other (NOT ROS2) libraries to
target_link_libraries
.
############################
# CPP Binary Block [BEGIN] #
# vvvvvvvvvvvvvvvvvvvvvvvv #
# https://ros2-tutorial.readthedocs.io/en/latest/
# While we cant use blocks https://cmake.org/cmake/help/latest/command/block.html#command:block
# we use set--unset
set(RCLCPP_LOCAL_BINARY_NAME print_forever_node)
add_executable(${RCLCPP_LOCAL_BINARY_NAME}
src/print_forever_node_main.cpp
src/print_forever_node.cpp
)
ament_target_dependencies(${RCLCPP_LOCAL_BINARY_NAME}
rclcpp
)
target_link_libraries(${RCLCPP_LOCAL_BINARY_NAME}
)
target_include_directories(${RCLCPP_LOCAL_BINARY_NAME} PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>)
target_compile_features(${RCLCPP_LOCAL_BINARY_NAME} PUBLIC c_std_99 cxx_std_17)
install(TARGETS ${RCLCPP_LOCAL_BINARY_NAME}
DESTINATION lib/${PROJECT_NAME})
unset(RCLCPP_LOCAL_BINARY_NAME)
# ^^^^^^^^^^^^^^^^^^^^^^ #
# CPP Binary Block [END] #
##########################
Create the package
Warning
We’ll skip using the --node-name
option to create the Node template, because, currently, it generates a Node and a CMakeLists.txt
different from my advice.
cd ~/ros2_tutorial_workspace/src
ros2 pkg create cpp_package_with_a_node \
--build-type ament_cmake \
--dependencies rclcpp
which outputs
ros2 pkg create output
going to create a new package
package name: cpp_package_with_a_node
destination directory: /home/murilo/ROS2_Tutorial/ros2_tutorial_workspace/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['murilo <murilomarinho@ieee.org>']
licenses: ['TODO: License declaration']
build type: ament_cmake
dependencies: ['rclcpp']
creating folder ./cpp_package_with_a_node
creating ./cpp_package_with_a_node/package.xml
creating source and include folder
creating folder ./cpp_package_with_a_node/src
creating folder ./cpp_package_with_a_node/include/cpp_package_with_a_node
creating ./cpp_package_with_a_node/CMakeLists.txt
[WARNING]: Unknown license 'TODO: License declaration'. This has been set in the package.xml, but no LICENSE file has been created.
It is recommended to use one of the ament license identitifers:
Apache-2.0
BSL-1.0
BSD-2.0
BSD-2-Clause
BSD-3-Clause
GPL-3.0-only
LGPL-3.0-only
MIT
MIT-0
Making C++ ROS2 Nodes
(Murilo’s) rclcpp
best practices
For each new C++ Node, we make three files following the style below.
For a Node called print_forever_node
we have
src/print_forever_node.hpp
with the Node’s class definition. In general, this is not exported to other packages, so it should not be in the package’sinclude
folder.src/print_forever_node.cpp
with the Node’s class implementation.src/print_forever_node_main.cpp
with the Node’s main function implementation.
In this step, we’ll work on these.
cpp_package_with_a_node
├── CMakeLists.txt
├── include
│ └── cpp_package_with_a_node
│ └── .placeholder
├── package.xml
└── src
├── print_forever_node.cpp
├── print_forever_node.hpp
└── print_forever_node_main.cpp
These files do not exists, so we’ll create them.
cd ~/ros2_tutorial_workspace/src/cpp_package_with_a_node
mkdir src
Similar to what we did in Python, we inherit from rclcpp::Node
. Whatever is different is owing to differences in languages.
1#pragma once
2
3#include <memory>
4#include <rclcpp/rclcpp.hpp>
5
6/**
7 * @brief A ROS2 Node that prints to the console periodically, but in C++.
8 */
9class PrintForeverNode: public rclcpp::Node
10{
11private:
12 double timer_period_;
13 int print_count_;
14 //also equivalent to rclcpp::TimerBase::SharedPtr
15 std::shared_ptr<rclcpp::TimerBase> timer_;
16
17 void _timer_callback();
18public:
19 PrintForeverNode();
20
21};
The implementation has nothing special, just don’t forget to initialize the parent class, rclcpp::Node
, with the name of the node.
1#include "print_forever_node.hpp"
2
3/**
4 * @brief PrintForeverNode::PrintForeverNode Default constructor.
5 */
6PrintForeverNode::PrintForeverNode():
7 rclcpp::Node("print_forever_cpp"),
8 timer_period_(0.5),
9 print_count_(0)
10{
11 //(Smart) pointers at the one thing that it doesn't matter much if they are not initialized in the member initializer list
12 //and this is a bit more readable.
13 timer_ = create_wall_timer(
14 std::chrono::milliseconds(long(timer_period_*1e3)),
15 std::bind(&PrintForeverNode::_timer_callback, this) //Note here the use of std::bind to build a single argument
16 );
17}
18
19/**
20 * @brief PrintForeverNode::_timer_callback periodically prints class info using RCLCPP_INFO.
21 */
22void PrintForeverNode::_timer_callback()
23{
24 RCLCPP_INFO_STREAM(get_logger(),
25 std::string("Printed ") +
26 std::to_string(print_count_) +
27 std::string(" times.")
28 );
29 print_count_++;
30}
Given that we are using rclcpp::spin()
, there is nothing special here either. Just remember to not mess up the std::make_shared
and always use perfect forwarding. See Perfect forwarding.
The rclcpp::spin()
handles the SIGINT
when we, for example, press CTRL+C on the terminal. It is not perfect, but it does the trick for simple nodes like this one.
1#include <rclcpp/rclcpp.hpp>
2
3#include "print_forever_node.hpp"
4
5int main(int argc, char** argv)
6{
7 rclcpp::init(argc,argv);
8
9 try
10 {
11 auto node = std::make_shared<PrintForeverNode>();
12
13 rclcpp::spin(node);
14 }
15 catch (const std::exception& e)
16 {
17 std::cerr << std::string("::Exception::") << e.what();
18 }
19
20 return 0;
21}
Add a .placeholder
if your include/<PACKAGE_NAME>
is empty
Warning
If you don’t do this and add this package as a git repository without any files on the include/
, CMake might return with an error when trying to compile your package.
cpp_package_with_a_node
├── CMakeLists.txt
├── include
│ └── cpp_package_with_a_node
│ └── .placeholder
├── package.xml
└── src
├── print_forever_node.cpp
├── print_forever_node.hpp
└── print_forever_node_main.cpp
Empty directories will not be tracked by git. A file has to be added to the index. We can create an empty file in the include
folder as follows
cd ~/ros2_tutorial_workspace/src/cpp_package_with_a_node/src
touch include/cpp_package_with_a_node/.placeholder
Running a C++ Node
As simple as it has always been, see Running a node (ros2 run).
ros2 run cpp_package_with_a_node print_forever_node
which returns
[INFO] [1688620414.406930812] [print_forever_node]: Printed 0 times.
[INFO] [1688620414.906890884] [print_forever_node]: Printed 1 times.
[INFO] [1688620415.406907619] [print_forever_node]: Printed 2 times.
[INFO] [1688620415.906881003] [print_forever_node]: Printed 3 times.
[INFO] [1688620416.406900108] [print_forever_node]: Printed 4 times.
[INFO] [1688620416.906886691] [print_forever_node]: Printed 5 times.
[INFO] [1688620417.406881803] [print_forever_node]: Printed 6 times.
[INFO] [1688620417.906858551] [print_forever_node]: Printed 7 times.
[INFO] [1688620418.406894922] [print_forever_node]: Printed 8 times.
and we’ll use CTRL+C to stop the node, resulting in
[INFO] [1688620418.725674401] [rclcpp]: signal_handler(signum=2)