Note: This is part of a series where I follow along with the ROS 2 Humble and Gazebo Fortress tutorials and demos. This is not original content.
Aaaand we’re back! Let’s follow along with this tutorial today, which will teach us how to create a simple ROS publisher/subscriber pair written in C++.
A quick refresher Link to heading
Remember that systems built using ROS consist of a set of nodes (processes) that communicate with each other over the ROS graph using topics.
- Nodes which send messages to a topic are publishers
- Nodes which receive the messages from the topic are subscribers
Creating a workspace and a package Link to heading
Let’s create a new workspace, then create a new package within the workspace:
mkdir -p ~/pubsub_ws
mkdir -p ~/pubsub_ws/src
cd ~/pubsub_ws/src
ros2 pkg create --build-type ament_cmake --license Apache-2.0 cpp_pubsub
That should return a message indicating that our package was created successfully:
ros2 pkg create --build-type ament_cmake --license Apache-2.0 cpp_pubsub
bash: cd: pubsub_ws/src: No such file or directory
going to create a new package
package name: cpp_pubsub
destination directory: /home/ubuntu
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['ubuntu <ubuntu@todo.todo>']
licenses: ['Apache-2.0']
build type: ament_cmake
dependencies: []
creating folder ./cpp_pubsub
creating ./cpp_pubsub/package.xml
creating source and include folder
creating folder ./cpp_pubsub/src
creating folder ./cpp_pubsub/include/cpp_pubsub
creating ./cpp_pubsub/CMakeLists.txt
Now, move to the folder which will hold the code for the package and fetch the example code from the ROS docs:
cd ~/pubsub_ws/src/cpp_pubsub/src
wget -O publisher_member_function.cpp https://raw.githubusercontent.com/ros2/examples/humble/rclcpp/topics/minimal_publisher/member_function.cpp
Open up publisher_member_function.cpp
and take a look at the top of the file. You’ll see some #include
statements there:
#include <chrono>
#include <functional>
#include <memory>
#include <string>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
These allow us to use some of ROS’s built in functonality. In particular, rclcpp.hpp
includes some basic ROS functionality, and std_msgs/msg/string.hpp
defines a built-in ROS message type.
These #includes
represent external code that we will depend on. Remember that we must also update package.xml
and CMakeLists.txt
to include all of our dependencies. We’ll do that later.
There is one class
in our .cpp
file:
class MinimalPublisher : public rclcpp::Node
The MinimalPublisher class inherits from one of the built-in classes provided by ROS, rclcpp::Node
.
The official docs spend a lot of time going through the body of the class line-by-line. Instead, I reproduce the whole thing here and then attempt to summarize the public
and private
blocks and their functions. First, here’s the whole thing:
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;
}
Public block Link to heading
See this block at the top?
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));
}
What does this code do? In short:
- Names the node
minimal_publisher
- Sets a message counter to
0
- Creates a topic that sends
String
and has a queue depth of 10 messages (if the depth exceeds 10, we start dropping messages) - Initializes
timer_
so thattimer_callback
will excute twice a second
Private block Link to heading
The class contains a private function called timer_callback()
:
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_;
};
What’s going on here?
- The data our messages will contain is set by the
timer_callback()
function - The macro
RCLCPP_INFO
makes sure messages get printed to the console - Timer, publisher, and counter fields are declared (bottom 3 lines just above
};
)
main
function
Link to heading
And of course as with any C or C++ program below all our #imports
and definitions, we have main()
, the code which actually gets run when our program is invoked:
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalPublisher>());
rclcpp::shutdown();
return 0;
}
What’s it do?
rclcpp::init
initializes ROS 2rclcpp::spin
starts processing data from the node (this includes our twice-a-second timer callbacks)rclcpp::shutdown()
gracefully shuts down our node if ROS 2 asks it to shut down
Adding dependencies Link to heading
We need to add our dependencies into CMakeLists.txt
and package.xml
to make sure the build process won’t fail.
Updating package.xml
Link to heading
Here’s a completed package.xml
, note the <depend>
lines at the bottom:
<?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>cpp_pubsub</name>
<version>0.0.0</version>
<description>TODO: Package description</description>
<maintainer email="ubuntu@todo.todo">ubuntu</maintainer>
<license>Apache-2.0</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<depend>rclcpp</depend>
<depend>std_msgs</depend>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>
Note: Don’t forget to update your <description>
, <maintainer>
, and <license>
.
Updating CMakeLists.txt
Link to heading
Here’s a completed CMakeLists.txt
, note the find_package()
lines at the bottom:
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()
We have removed some if(BUILD_TESTING)
code which is part of default, newly-created CMakeLists.txt
files, as we don’t need it here.
Create the subscriber node Link to heading
Now we repeat the process above to creat ea subscriber:
cd ~/pubsub_ws/src/cpp_pubsub/src/
wget -O subscriber_member_function.cpp https://raw.githubusercontent.com/ros2/examples/humble/rclcpp/topics/minimal_subscriber/member_function.cpp
There should now be two .cpp
files in src
:
publisher_member_function.cpp
subscriber_member_function.cpp
Once again let’s look at the whole code before breaking it into chunks and summarizing what each chunk does:
#include <functional>
#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;
}
The code is almost exactly the same so we. don’t need to spend as much time dissecting it. Important bits are:
topic_callback
which receives messages published over the topicRCLCPP_INFO
which once again ensures messages are written to the console- A single field declaration for a field called
subscription
which receivesString
messages
And of cousre there is a main()
function, which in this case sets up our MinimalSubscriber
node and starts listening for messages.
Note: The dependencies for the subcsriber are the same as those for the publisher, so we don’t need to add new dependencies package.xml
or CMakeLists.txt
. However we do need to make some small chagnes to CMakeLists.txt
, covered in the next section.
Final edits to CMakeLists.txt
Link to heading
Under the entries added for the publisher, we add a new install
field. Here’s the new complete CmakeLists.txt
file:
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})
add_executable(listener src/subscriber_member_function.cpp)
ament_target_dependencies(listener rclcpp std_msgs)
install(TARGETS
talker
listener
DESTINATION lib/${PROJECT_NAME})
ament_package()
Build the package Link to heading
Now we can build and run the code. First, make sure all the required dependencies are installed:
cd ~/pubsub_ws/
rosdep install -i --from-path src --rosdistro humble -y
Then we build with:
colcon build --packages-select cpp_pubsub
Run the code Link to heading
Per usual we need to add our workspace as a overlay with source
. A shortcut to do this is:
. install/setup.bash
We can now run the code
ros2 run cpp_pubsub talker
Which will output:
[INFO] [1723714559.668716716] [minimal_publisher]: Publishing: 'Hello, world! 0'
[INFO] [1723714560.168713865] [minimal_publisher]: Publishing: 'Hello, world! 1'
[INFO] [1723714560.668717908] [minimal_publisher]: Publishing: 'Hello, world! 2'
[INFO] [1723714561.168717701] [minimal_publisher]: Publishing: 'Hello, world! 3'
[INFO] [1723714561.668719724] [minimal_publisher]: Publishing: 'Hello, world! 4'
[INFO] [1723714562.168721101] [minimal_publisher]: Publishing: 'Hello, world! 5'
[INFO] [1723714562.668716040] [minimal_publisher]: Publishing: 'Hello, world! 6'
[INFO] [1723714563.168737803] [minimal_publisher]: Publishing: 'Hello, world! 7'
[INFO] [1723714563.668727827] [minimal_publisher]: Publishing: 'Hello, world! 8'
[INFO] [1723714564.168715178] [minimal_publisher]: Publishing: 'Hello, world! 9'
[INFO] [1723714564.668721436] [minimal_publisher]: Publishing: 'Hello, world! 10'
[INFO] [1723714565.168721578] [minimal_publisher]: Publishing: 'Hello, world! 11'
[INFO] [1723714565.668722939] [minimal_publisher]: Publishing: 'Hello, world! 12'
[INFO] [1723714566.168723089] [minimal_publisher]: Publishing: 'Hello, world! 13'
[INFO] [1723714566.668725820] [minimal_publisher]: Publishing: 'Hello, world! 14'
[INFO] [1723714567.168726418] [minimal_publisher]: Publishing: 'Hello, world! 15'
[INFO] [1723714567.668728895] [minimal_publisher]: Publishing: 'Hello, world! 16'
[INFO] [1723714568.168725383] [minimal_publisher]: Publishing: 'Hello, world! 17'
[INFO] [1723714568.668725291] [minimal_publisher]: Publishing: 'Hello, world! 18'
[INFO] [1723714569.168725436] [minimal_publisher]: Publishing: 'Hello, world! 19'
We then do more or less the same thing in a new terminal window, except we run the listener:
. install/setup.bash
ros2 run cpp pubsub listener
Which will output something like this (with the message numbers depending on how much later you started the listener than the talker):
[INFO] [1723714564.168927211] [minimal_subscriber]: I heard: 'Hello, world! 9'
[INFO] [1723714564.668937723] [minimal_subscriber]: I heard: 'Hello, world! 10'
[INFO] [1723714565.168926458] [minimal_subscriber]: I heard: 'Hello, world! 11'
[INFO] [1723714565.668908873] [minimal_subscriber]: I heard: 'Hello, world! 12'
[INFO] [1723714566.168936092] [minimal_subscriber]: I heard: 'Hello, world! 13'
[INFO] [1723714566.668940297] [minimal_subscriber]: I heard: 'Hello, world! 14'
[INFO] [1723714567.168951832] [minimal_subscriber]: I heard: 'Hello, world! 15'
[INFO] [1723714567.668957725] [minimal_subscriber]: I heard: 'Hello, world! 16'
[INFO] [1723714568.168941230] [minimal_subscriber]: I heard: 'Hello, world! 17'
[INFO] [1723714568.668918680] [minimal_subscriber]: I heard: 'Hello, world! 18'
[INFO] [1723714569.168950312] [minimal_subscriber]: I heard: 'Hello, world! 19'
Try stopping the talker (publisher) first and then seeing what happens to the listener. That’s right! It stopped printing messages to the console (because it is no longer receiving any messages from the publisher).
That’s it for this time. Next time, we’ll look at creating a simpe publisher/subscriber using Python.
For more pub/sub examples, look here.