In this ROS2 Jazzy Tutorial, we thoroughly explain how to write subscriber and publisher nodes in C++ from scratch. We use a ROS2 Jazzy Jalisco distribution and Linux Ubuntu 24.04. The techniques and material that you will learn in this tutorial are of paramount importance for the development of the ROS2 programs. The YouTube tutorial accompanying this webpage is given below.
Copyright notice: This document, developed codes, and the lesson video should not be copied, redistributed, or publicly posted on public or private websites or social media platforms. The developed codes, this manual, and the video are the property of the author (Aleksandar Haber). Without the author’s permission, this video and written tutorial as well as developed codes should not be used as an official or unofficial study or lecture material in university and non-university courses, as well as in online learning commercial platforms such as Udemy, Coursera, Udacity, and similar learning platforms. Without the author’s permission, the codes should not be included in academic works, student reports, and student papers or theses. Also, the developed codes should not be used in commercial settings without paying an appropriate fee and without the permission of the author. Finally, this website and accompanying video should NOT be used to train an AI, machine learning, and large language model. That is, the material presented here should not be exploited by an AI algorithm to train a neural network that will reproduce the content of this website in any way.
STEP 1: Verify the Linux and ROS2 Distributions
The first step is to verify that you have the correct Linux and ROS2 distributions. Namely, this tutorial is created under the assumption that you are using Linux Ubuntu 24.04 (Noble) and ROS2 Jazzy Jalisco. To check the Linux distribution
lsb_release -a
The output is
No LSB modules are available.
Distributor ID: Ubuntu
Description: Ubuntu 24.04 LTS
Release: 24.04
Codename: noble
Another approach for verifying the Linux distribution is
cat /etc/os-release
The output is
PRETTY_NAME="Ubuntu 24.04 LTS"
NAME="Ubuntu"
VERSION_ID="24.04"
VERSION="24.04 LTS (Noble Numbat)"
VERSION_CODENAME=noble
ID=ubuntu
ID_LIKE=debian
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
UBUNTU_CODENAME=noble
LOGO=ubuntu-logo
This means that we have the correct Linux distribution Ubuntu 24.04. Next, let us check that we have the correct Linux distribution ROS2 Jazzy. To do that, type:
cd /opt/ros
ls -la
You should see as an output a folder called “jazzy”. If that is the case, then ROS2 Jazzy is installed. However, we need to perform one additional check. Type:
source /opt/ros/jazzy/setup.bash
printenv ROS_DISTRO
The output should be “jazzy”. This means that ROS2 Jazzy Jalisco is installed on the computer.
STEP 2: Create the workspace folder and the ROS2 package
First, let us run
sudo apt update && sudo apt upgrade
Then, let install gedit (simple and easy to use a Linux editor)
sudo apt install gedit
Colcon should be automatically installed together with ROS2. However, run this just in case, if Colcon for some reason is not installed with ROS2
sudo apt install python3-colcon-common-extensions
To be able to create subscriber and published nodes, we need to create a workspace folder and a ROS2 package. First, open a new terminal window and in that terminal window source the environment (remember to do this whenever you start a new terminal and you want to use ROS2):
source /opt/ros/jazzy/setup.bash
Then, after sourcing the environment, we need to create the main workspace folder and inside of this folder, we need to create a subfolder called “src”. We do that by typing
cd ~
mkdir -p ~/ws_sub_pub/src
Here, the name of the workspace folder is “ws_sub_pub”, where “sub” is short for subscriber and “pub” is short for the publisher. Then, let us navigate to the new “src” folder by typing:
cd ~/ws_sub_pub/src
Here, you should keep in mind that it is a standard ROS2 practice to create packages in the src folder! Then, run
ros2 pkg create --build-type ament_cmake sub_pub
To create the package. The package name is “sub_pub”.
STEP 3: Create publisher and subscriber files
Let us create the publisher C++ file:
cd ~/ws_sub_pub/src/sub_pub/src
gedit publisher.cpp
The C++ publisher file is given below
// string library
#include <string>
// time library
#include <chrono>
// library used to manage dynamic memory
#include <memory>
//we will send std_msgs of the type string, that is, we will send strings
#include "std_msgs/msg/string.hpp"
//C++ ROS2 Client Library API
// https://docs.ros2.org/latest/api/rclcpp/index.html
#include "rclcpp/rclcpp.hpp"
// we use this namespace
using namespace std::chrono_literals;
// here we embed the publisher in a C++ class
// the class is inherited from ROS2 Node class
class SimplePublisher : public rclcpp::Node
{
public:
// constructor
SimplePublisher() : Node("publisher_node")
{
// CounterValue is a private variable used to count the number of messages
// see the declaration in the private member variable section
CounterValue =0;
// publisherObject is a private variable/object
// here, we are creating a publisher object,
// we specify
// - the type of the message that we want to send: "std_msgs::msg::String"
// - name of the topic "communication_topic" (this has to match the name of the topic in the subscriber)
// - buffer size - 20
publisherObject = this->create_publisher<std_msgs::msg::String>("communication_topic", 20);
// this is the callback function that actually sends the messages
// this is a lambda function, no input arguments, returns void
// performs a by-reference capture of the current object "this"
// https://en.cppreference.com/w/cpp/language/lambda
auto callBackFunction =
[this]() -> void {
// increase the counter
CounterValue++;
// create an empty message that we want to send
auto message = std_msgs::msg::String();
// fill-in the message content
message.data = "Message Number " + std::to_string(this->CounterValue);
// print a message to the terminal of the publisher node
RCLCPP_INFO(this->get_logger(), "Published Message: '%s'", message.data.c_str());
// publish the message over the corresponding topic
this->publisherObject->publish(message);
};
// create a timer object
// 1000ms is a period of calling the call back function
// the second input argument is the name of the call back function
timerObject = this->create_wall_timer(1000ms, callBackFunction);
}
private:
// timer
rclcpp::TimerBase::SharedPtr timerObject;
// publisher
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisherObject;
// counter
int CounterValue;
};
// main function
int main(int argc, char * argv[])
{
// initialize ROS2
rclcpp::init(argc, argv);
// spins the node - the callback function is called
rclcpp::spin(std::make_shared<SimplePublisher>());
// shutdown after the user presses CTRL+C
rclcpp::shutdown();
return 0;
}
Then, let us create the subscriber C++ file:
gedit subscriber.cpp
The C++ subscriber file is given below
// for memory
#include <memory>
//we will send std_msgs of the type string, that is, we will send strings
#include "std_msgs/msg/string.hpp"
//C++ ROS Client Library API
// https://docs.ros2.org/latest/api/rclcpp/index.html
#include "rclcpp/rclcpp.hpp"
// inherits from Node
class SimpleSubscriber : public rclcpp::Node
{
public:
SimpleSubscriber(): Node("subscriber_node")
{
// lambda function whose purpose is similar to the lambda function in the subscriber node
// here, the input is actually the msg that is sent throught the topic
auto callBackFunction =
[this](std_msgs::msg::String::UniquePtr msg) -> void {
// here, we just print the message in the terminal window
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str());
};
// here, we create a subscriber, we call the function from the Node class
// the first input is the name of the topic over which we communicate and
// this name has to match the name in the publisher node
// the second input is the buffer size
// the third input is the name of the callback function that is called
// once the message is detected
subscriberObject =
this->create_subscription<std_msgs::msg::String>("communication_topic", 20, callBackFunction);
}
private:
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscriberObject;
};
int main(int argc, char * argv[])
{
// initialize ROS2
rclcpp::init(argc, argv);
// spins the node - the callback function is called
rclcpp::spin(std::make_shared<SimpleSubscriber>());
// shutdown after the user presses CTRL+C
rclcpp::shutdown();
return 0;
}
STEP 4: Add Dependencies
We need to edit two files: “package.xml” and “CMakeLists.txt”. They are located in the folder source folder of our package: “~/ws_sub_pub/src/sub_pub”. Let us navigate to that folder
cd ~/ws_sub_pub/src/sub_pub
Then, let us edit the file called “package.xml”. We do that by typing
gedit package.xml
Since both the publisher and subscriber C++ files depend on “rclcpp” and “std_msgs” header files, add these two lines to the package.xml below last “<test_depend>” tag:
<exec_depend>rclcpp</exec_depend>
<exec_depend>std_msgs</exec_depend>
The next step is to edit “CMakeLists.txt”. We do that by typing:
gedit CMakeLists.txt
Then, just below the line “find_package(ament_cmake REQUIRED)“, add
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
Then, add the following four lines immediately below these two lines:
add_executable(publisher_node src/publisher.cpp)
ament_target_dependencies(publisher_node rclcpp std_msgs)
add_executable(subscriber_node src/subscriber.cpp)
ament_target_dependencies(subscriber_node rclcpp std_msgs)
These four lines will create executable files called: “publisher_node” and “subscriber_node” and they will specify the source files and dependencies. Finally, add this
install(TARGETS
publisher_node
subscriber_node
DESTINATION lib/${PROJECT_NAME})
so ROS2 can find the executable files.
STEP 5: Final Build
Navigate to the root folder of the workspace by typing:
cd ~/ws_sub_pub/
And then run
sudo rosdep init
rosdep update
rosdep install -i --from-path src --rosdistro jazzy -y
To check for missing the dependencies (the two libraries included should come with the standard ROS2 installation). You should get this output
#All required rosdeps installed successfully
To build everything just type
colcon build
Or you can directly select the package that you want to build by typing:
colcon build --packages-select sub_pub
STEP 6: Final Step
Open a new terminal, and source the created package by typing:
source ~/ws_sub_pub/install/setup.bash
To run the publisher node type
ros2 run sub_pub publisher_node
You should see the output:
[INFO] [1721017801.710906019] [publisher_node]: Published Message: 'Message Number 1'
[INFO] [1721017802.710924174] [publisher_node]: Published Message: 'Message Number 2'
[INFO] [1721017803.710942483] [publisher_node]: Published Message: 'Message Number 3'
[INFO] [1721017804.710906846] [publisher_node]: Published Message: 'Message Number 4'
[INFO] [1721017805.710922945] [publisher_node]: Published Message: 'Message Number 5'
[INFO] [1721017806.710925642] [publisher_node]: Published Message: 'Message Number 6'
[INFO] [1721017807.710928530] [publisher_node]: Published Message: 'Message Number 7'
[INFO] [1721017808.710828761] [publisher_node]: Published Message: 'Message Number 8'
On the other hand, to run the subscriber node, open another terminal while the terminal running the publisher node is active, and type
source ~/ws_sub_pub/install/setup.bash
ros2 run sub_pub subscriber_node
The output should look like this
[INFO] [1721017825.711327944] [subscriber_node]: I heard: 'Message Number 25'
[INFO] [1721017826.710891068] [subscriber_node]: I heard: 'Message Number 26'
[INFO] [1721017827.711335487] [subscriber_node]: I heard: 'Message Number 27'
[INFO] [1721017828.711413193] [subscriber_node]: I heard: 'Message Number 28'
[INFO] [1721017829.711322701] [subscriber_node]: I heard: 'Message Number 29'
[INFO] [1721017830.711345099] [subscriber_node]: I heard: 'Message Number 30'
[INFO] [1721017831.711328804] [subscriber_node]: I heard: 'Message Number 31'
[INFO] [1721017832.711346919] [subscriber_node]: I heard: 'Message Number 32'
[INFO] [1721017833.711344817] [subscriber_node]: I heard: 'Message Number 33'
[INFO] [1721017834.711391903] [subscriber_node]: I heard: 'Message Number 34'
[INFO] [1721017835.711277406] [subscriber_node]: I heard: 'Message Number 35'
[INFO] [1721017836.711454820] [subscriber_node]: I heard: 'Message Number 36'
[INFO] [1721017837.711375222] [subscriber_node]: I heard: 'Message Number 37'
[INFO] [1721017838.711406972] [subscriber_node]: I heard: 'Message Number 38'
[INFO] [1721017839.711473441] [subscriber_node]: I heard: 'Message Number 39'
[INFO] [1721017840.711422701] [subscriber_node]: I heard: 'Message Number 40'