November 21, 2024

How to Write Publisher and Subscriber Nodes in C++ from Scratch in ROS2 Jazzy Jalisco

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'