Create a Publisher and Subscriber in C++ | ROS 2 Foxy Fitzroy

cover_talk_telephone_communication_phone

In this post, we will learn how to create a publisher node, subscriber node, and a publishing subscriber node in ROS 2 Foxy Fitzroy using C++. You can think of a node as a small single-purpose program within a larger robotic system. Publisher nodes publish data to topic(s), subscriber nodes receive data from topic(s), and a publishing subscriber node can do both receive data from topic(s) and publish data to topic(s).

A good analogy is YouTube. YouTubers (publisher nodes) publish videos (messages) to a channel (topic), and you (subscriber node) can subscribe to that channel (topic) so that you receive all the videos (messages) on that channel (topic). 

Robotic systems (mobile robots, robotic arms, and aeriel robots) pass a lot of data around, which is why the ROS 2 framework for developing robots is so useful and popular.

The official tutorial is located in the ROS 2 Foxy documentation, but we’ll run through the entire process step-by-step below.

You Will Need

In order to complete this tutorial, you will need:

Prerequisites

You have already created a workspace.

Create a Package

Open a new terminal window, and navigate to the src directory of your workspace:

cd ~/dev_ws/src

Now let’s create a package named cpp_pubsub.

Type this command:

ros2 pkg create --build-type ament_cmake cpp_pubsub

Your package named cpp_pubsub has now been created.

1-create-a-new-packageJPG

Write a Publisher Node

Move to the /dev_ws/src/cpp_pubsub/src folder

cd cpp_pubsub/src

This folder is where our source code (i.e. publisher and subscriber nodes) will reside.

Make sure you have a text editor installed. I like to use gedit.

sudo apt-get install gedit

Create a blank C++ file called my_publisher_node.cpp.

gedit my_publisher_node.cpp

Write the following code. Don’t be intimidated. C++ code can look kind of scary. Just go one line at a time, and read the comments to understand what each line does.

This publisher node publishes the message “Hi Automatic Addison!” every 500 milliseconds (i.e. two times per second) to a topic named addison.

Note that in this tutorial, we are publishing a string to a topic. In a real robotics project, you’ll typically be publishing numerical values. We’re just using strings in this example as a demonstration.

// Include important C++ header files that provide class
// templates for useful operations.
#include <chrono> // Date and time
#include <functional> // Arithmetic, comparisons, and logical operations
#include <memory> // Dynamic memory management
#include <string> // String functions

// ROS Client Library for C++
// Allows use of the most common elements of ROS 2
#include "rclcpp/rclcpp.hpp"

// Built-in message type that will be used to publish data
#include "std_msgs/msg/string.hpp"

// chrono_literals handles user-defined time durations (e.g. 500ms) 
using namespace std::chrono_literals;

// Create the node class named MinimalPublisher which inherits the attributes
// and methods of the rclcpp::Node class.
class MinimalPublisher : public rclcpp::Node
{
  public:
    // Constructor creates a node named minimal_publisher. 
    // The published message count is initialized to 0.
    MinimalPublisher()
    : Node("minimal_publisher"), count_(0)
    {
      // Publisher publishes String messages to a topic named "addison". 
      // The size of the queue is 10 messages.
      publisher_ = this->create_publisher<std_msgs::msg::String>("addison",10);
      
      // Initialize the timer. The timer_callback function will execute every
      // 500 milliseconds.
      timer_ = this->create_wall_timer(
      500ms, std::bind(&MinimalPublisher::timer_callback, this));
    }

  private:
    // This method executes every 500 milliseconds
    void timer_callback()
    {
      // Create a new message of type String
      auto message = std_msgs::msg::String();
      
      // Set our message's data attribute and increment the message count by 1
      message.data = "Hi Automatic Addison! " + std::to_string(count_++);

      // Print every message to the terminal window      
      RCLCPP_INFO(this->get_logger(),"Publishing: '%s'", message.data.c_str());
      
      // Publish the message to the topic named "addison"
      publisher_->publish(message);
    }
    
    // Declaration of the timer_ attribute
    rclcpp::TimerBase::SharedPtr timer_;
 
    // Declaration of the publisher_ attribute
    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
  
    // Declaration of the count_ attribute
    size_t count_;
};

// Node execution starts here
int main(int argc, char * argv[])
{
  // Initialize ROS 2
  rclcpp::init(argc, argv);
 
  // Start processing data from the node as well as the callbacks and the timer
  rclcpp::spin(std::make_shared<MinimalPublisher>());

  // Shutdown the node when finished
  rclcpp::shutdown();
  return 0;
}

Add Dependencies

Now that we’ve written our publisher node, we need to let our system know what libraries our node needs in order to execute properly. These libraries are our node’s dependencies.

Go to the dev_ws/src/cpp_pubsub folder, and see what files are in there.

cd ~/dev_ws/src/cpp_pubsub
dir
2-what-files-in-thereJPG

You should see that this folder contains a file named package.xml and a file named CMakeLists.txt.

The package.xml file contains key information about the cpp_pubsub package. CMakeLists.txt contains important information needed to compile the C++ source code you wrote in the previous section of this tutorial.

Open the package.xml file. 

gedit package.xml

Fill in the description of the cpp_pubsub package, your email address and name on the maintainer line, and the license you desire (e.g. Apache License 2.0).

<description>A minimal publisher/subscriber that uses the ROS C++ Client Library</description>
<maintainer email="automaticaddison@todo.todo">automaticaddison</maintainer>
<license>Apache License 2.0</license>

Now, after the <build_type>ament_cmake</build_type> line, add the two dependencies your node needs in order to compile. These properties were included at the top of the C++ code of your publisher node (the other dependencies are part of the C++ Standard Library, so they don’t need to be added here).

<depend>rclcpp</depend>
<depend>std_msgs</depend>
3-package-xml-fileJPG

Save the file and close it to return to the terminal window.

Modify CMakeLists.txt

Open CMakeLists.txt.

gedit CMakeLists.txt

Under this line (find_package(ament_cmake REQUIRED)), add the dependencies:

find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

Below that, add the executable. We will name it my_publisher.

add_executable(my_publisher src/my_publisher_node.cpp)
ament_target_dependencies(my_publisher rclcpp std_msgs)

Below that, add the install section so that ROS 2 will be able to find the executable.

install(TARGETS
  my_publisher
  DESTINATION lib/${PROJECT_NAME})
4-cmakelistsJPG

Save the file and close it.

Write a Subscriber Node

Now let’s write a subscriber node. This node will subscribe to String messages that are published by the publisher node to the addison topic.

Navigate to the dev_ws/src/cpp_pubsub/src directory.

cd ~/dev_ws/src/cpp_pubsub/src

Create a blank C++ file called my_subscriber_node.cpp.

gedit my_subscriber_node.cpp

Write the following code.

// Include the C++ standard library headers
#include <memory> // Dynamic memory management

// Dependencies
#include "rclcpp/rclcpp.hpp" // ROS Clienty Library for C++
#include "std_msgs/msg/string.hpp" // Handles String messages in ROS 2
using std::placeholders::_1;

class MinimalSubscriber : public rclcpp::Node
{
  public:
    // Constructor
    // The name of the node is minimal_subscriber
    MinimalSubscriber()
    : Node("minimal_subscriber")
    {
      // Create the subscription.
      // The topic_callback function executes whenever data is published
      // to the 'addison' topic.
      subscription_ = this->create_subscription<std_msgs::msg::String>(
      "addison", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
    }

  private:
    // Receives the String message that is published over the topic
    void topic_callback(const std_msgs::msg::String::SharedPtr msg) const
    {
      // Write the message that was received on the console window
      RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str());
    }
    // Declare the subscription attribute
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};

int main(int argc, char * argv[])
{
  // Launch ROS 2
  rclcpp::init(argc, argv);
  
  // Prepare to receive messages that arrive on the topic
  rclcpp::spin(std::make_shared<MinimalSubscriber>());
  
  // Shutdown routine for ROS2
  rclcpp::shutdown();
  return 0;
}

Save the file, and close it.

Add Dependencies

We don’t need to add anything to our package.xml file since the dependencies are the same as the publisher node’s dependencies.

Modify CMakeLists.txt

Go to the dev_ws/src/cpp_pubsub folder, and see what files are in there.

cd ~/dev_ws/src/cpp_pubsub

Open CMakeLists.txt.

gedit CMakeLists.txt

Under this line (ament_target_dependencies(my_publisher rclcpp std_msgs)), add the executable path and the dependencies for the subscriber node you just created. We will name the executable my_subscriber

add_executable(my_subscriber src/my_subscriber_node.cpp)
ament_target_dependencies(my_subscriber rclcpp std_msgs)

Now add this after my_publisher inside the install section

my_subscriber
5-new-cmakelistsJPG

Save the file, and close it.

Build the Package 

Return to the root of your workspace:

cd ~/dev_ws/

We need to double check that all the dependencies needed (rclcpp and std_msgs) are already installed.

rosdep install -i --from-path src --rosdistro foxy -y
6-all-dependencies-installedJPG

Open your bash file, and make sure these two lines are at the bottom of your file.

7-these-two-linesJPG

Build all packages in the workspace.

colcon build

Alternatively, if you only want to build cpp_pubsub and no other package in the workspace, you can type:

colcon build --packages-select cpp_pubsub

The build was successful.

8-build-successfulJPG

The executable is located inside the dev_ws/install/cpp_pubsub/lib/cpp_pubsub folder.

Run the Nodes

To run the nodes, open a new terminal window.

Make sure you are in the root of your workspace:

cd ~/dev_ws/

Run the publisher node. If you recall, its name is my_publisher.

ros2 run cpp_pubsub my_publisher

You will see a message published every 500 milliseconds.

9-run-publisherJPG

Open a new terminal, and run the subscriber node.

ros2 run cpp_pubsub my_subscriber
10-run-subscriberJPG

Let’s see what topics are currently active. Open a new terminal, and type:

ros2 topic list -t
11-active-topicsJPG

Let’s listen to the addison topic.

ros2 topic echo /addison
14d-listen-to-addison2-topic

Press CTRL + C

Now let’s see the node graph.

rqt_graph

You can see the relationship between the nodes. 

13-relationship-between-nodesJPG

minimal_publisher publishes data to the /addison topic. minimal_subscriber subscribes to data published on that topic.

Press CTRL + C in all terminals to shutdown the programs.

Create a Launch File

Now let’s create a ROS 2 launch file. This launch file will enable us to launch the publisher and subscriber nodes simultaneously with a single command.

Go to the following directory

cd ~/dev_ws/src/cpp_pubsub/

Type the following command to create a new folder:

mkdir launch

Move inside that folder.

cd launch

Open up a new launch file.

gedit cpp_pubsub_launch.py

Write the following code inside the launch file.

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
       Node(
         package='cpp_pubsub',
         namespace='ns1', # Make sure this matches the subscriber's namespace
         executable='my_publisher', # Name of the executable
         name='minimal_publisher' # Any name is fine
       ),
       Node(
         package='cpp_pubsub',
         namespace='ns1', # Make sure this matches the publisher's namespace
         executable='my_subscriber', # Name of the executable
         name='minimal_subscriber' # Any name is fine
       )
    ])

Click Save and close the file to return to the terminal.

Launch the Launch File

To launch the launch file, open a new terminal window, and move to the launch folder.

cd ~/dev_ws/src/cpp_pubsub/launch

Type:

ros2 launch cpp_pubsub_launch.py

Here was my output:

14-launch-fileJPG

Click Save and close the file to return to the terminal.

Create a Publishing Subscriber Node

Finally, let’s create a publishing subscriber node. This kind of node subscribes to a topic and publishes to another topic. 

A publishing subscriber node is common in robotics. Consider a robotic arm for example. You might have a node that reads camera data (i.e. subscribes), does some calculations, and then publishes servo motor angles to a topic.

Move to the /dev_ws/src/cpp_pubsub/src folder

cd ~/dev_ws/src/cpp_pubsub/src

Create a blank C++ file called pubsub_node.cpp.

gedit pubsub_node.cpp

Write the following code. 

This node subscribes to the addison topic and then publishes messages to the addison2 topic.

// Include important C++ header files that provide class
// templates for useful operations.
#include <functional> // Arithmetic, comparisons, and logical operations
#include <memory> // Dynamic memory management
#include <string> // String functions

// ROS Client Library for C++
// Allows use of the most common elements of ROS 2
#include "rclcpp/rclcpp.hpp"

// Built-in message type that will be used to publish data
#include "std_msgs/msg/string.hpp"

using std::placeholders::_1;

// Create the node class named PublishingSubscriber which inherits the attributes
// and methods of the rclcpp::Node class.
class PublishingSubscriber : public rclcpp::Node
{
  public:
    // Constructor creates a node named publishing_subscriber. 
    // The published message count is initialized to 0.
    PublishingSubscriber()
    : Node("publishing_subscriber")
    {
      // Create the subscription.
      // The topic_callback function executes whenever data is published
      // to the 'addison' topic.
      subscription_ = this->create_subscription<std_msgs::msg::String>(
      "addison", 10, std::bind(&PublishingSubscriber::topic_callback, this, _1));
			
      // Publisher publishes String messages to a topic named "addison2". 
      // The size of the queue is 10 messages.
      publisher_ = this->create_publisher<std_msgs::msg::String>("addison2",10);
      
    }

  private:
    // Receives the String message that is published over the topic
    void topic_callback(const std_msgs::msg::String::SharedPtr msg) const
    {
      // Create a new message of type String
      auto message = std_msgs::msg::String();
      
      // Set our message's data attribute
      message.data = "I heard " + msg->data;

      // Publish the message to the topic named "addison2"
      publisher_->publish(message);
    }
    // Declare the subscription attribute
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
		
    // Declaration of the publisher_ attribute		
    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
  
};

// Node execution starts here
int main(int argc, char * argv[])
{
  // Initialize ROS 2
  rclcpp::init(argc, argv);
 
  // Start processing data from the node as well as the callbacks
  rclcpp::spin(std::make_shared<PublishingSubscriber>());

  // Shutdown the node when finished
  rclcpp::shutdown();
  return 0;
}

Go to the dev_ws/src/cpp_pubsub folder.

cd ~/dev_ws/src/cpp_pubsub

Open CMakeLists.txt.

gedit CMakeLists.txt

Add the executable path underneath the ament_target_dependencies(my_subscriber…) line.

add_executable(pubsub_node src/pubsub_node.cpp)
ament_target_dependencies(pubsub_node rclcpp std_msgs)

Now add this after my_subscriber inside the install section

pubsub_node
14b-pubsub-1

Save the file, and close it.

Return to the root of your workspace:

cd ~/dev_ws/

We need to double check that all the dependencies needed (rclcpp and std_msgs) are already installed.

rosdep install -i --from-path src --rosdistro foxy -y

Build all packages in the workspace.

colcon build

The build was successful.

Open a new terminal window.

Make sure you are in the root of your workspace:

cd ~/dev_ws/

Run the publisher node. 

ros2 run cpp_pubsub my_publisher

Open a new terminal, and run the publishing subscriber node.

ros2 run cpp_pubsub pubsub_node

You will not see any output from running this node, but we can see it if we look at the addison2 topic.

Let’s see what topics are currently active. Open a new terminal, and type:

ros2 topic list -t
14c-active-topics

Let’s listen to the addison2 topic.

ros2 topic echo /addison2
14d-listen-to-addison2-topic-1

Press CTRL + C in all terminals to shutdown the programs.

Zip the Package for Distribution

If you ever want to zip the package and send it to someone, open a new terminal window.

Move to the directory containing your package.

cd ~/dev_ws/src
zip -r cpp_pubsub.zip cpp_pubsub
15-zip-fileJPG

The syntax is:

zip -r <filename.zip> <foldername>

Final Words

Creating C++ publishers and subscribers is a lot more tedious than creating Python publishers and subscribers. But while C++ publishers and subscribers are more complex to get up and running, they tend to have much faster execution time compared to their Python counterparts.

In the next post, we’ll take a look at how to create a basic publisher and subscriber for ROS 2 using Python.