Creating Publisher and Subscriber Nodes in ROS 2 with C++

Introduction to ROS2

Last updated: December 22, 2024

1. Introduction

In this lesson, we will learn how to create ROS 2 publisher and subscriber nodes using C++. We will:

Complete codeComplete code: talker_listener_cpp on Edreate GitHub

1a. What is a ROS 2 Package?

A ROS 2 package is a structured directory that contains nodes, libraries, launch files, and configuration files. It acts as a modular component of your ROS 2 project. The package is defined by a package.xml file and typically built with colcon. For C++-based ROS 2 nodes, we often use the ament_cmake build type.

1b. What is a Publisher?

A publisher is a ROS 2 node element that sends messages over a specified topic. Other nodes can subscribe to that topic to receive data. Publishers are created using create_publisher(), where you specify the message type, topic name, and queue size.

1c. What is a Subscriber?

A subscriber is a ROS 2 node element that listens for messages on a specified topic. It uses a callback function to process incoming data. Subscribers are created using create_subscription(), providing the message type, topic name, callback, and queue size.


2. Step-by-Step Implementation

2a. Prerequisites

Make sure you have:

Install necessary dependencies if you haven’t already:

sudo apt update
sudo apt install python3-colcon-common-extensions

2b. Set Up the Workspace

Create a new workspace and source ROS 2:

mkdir -p ~/ros2_ws/src
cd ~/ros2_ws
source /opt/ros/humble/setup.bash

Create a new C++-based ROS 2 package named talker_listener:

ros2 pkg create --build-type ament_cmake --node-name talker talker_listener_cpp
Terminal Output Example:
going to create a new package
package name: talker_listener_cpp
destination directory: /home/edreate/Desktop/ROS2Introduction/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['edreate <edreate@todo.todo>']
licenses: ['TODO: License declaration']
build type: ament_cmake
dependencies: []
node_name: talker
creating folder ./talker_listener_cpp
creating ./talker_listener_cpp/package.xml
creating source and include folder
creating folder ./talker_listener_cpp/src
creating folder ./talker_listener_cpp/include/talker_listener_cpp
creating ./talker_listener_cpp/CMakeLists.txt
creating ./talker_listener_cpp/src/talker.cpp

[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

You now have a talker_listener package with a basic structure. To check the structre run the tree command:

sudo apt install tree

# In src folder run
tree talker_listener_cpp/

Output:

talker_listener_cpp/
├── CMakeLists.txt
├── include
│   └── talker_listener_cpp
├── package.xml
└── src
    └── talker.cpp

3 directories, 3 files

2c. Create the Talker (Publisher)

Navigate into the talker_listener directory and edit the file named talker.cpp (or create it if not existing):

talker.cpp:

#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>

#include <chrono>
#include <string>

using namespace std::chrono_literals;

class Talker : public rclcpp::Node {
public:
  Talker() : Node("talker_cpp"), count_(0) {
    // Create publisher
    publisher_ = this->create_publisher<std_msgs::msg::String>("talker", 10);

    // Create timer
    timer_ =
        this->create_wall_timer(1s, std::bind(&Talker::publish_message, this));
  }

private:
  void publish_message() {
    auto message = std_msgs::msg::String();
    message.data = "Hello from talker_cpp: " + std::to_string(count_++);
    publisher_->publish(message);

    RCLCPP_INFO(this->get_logger(), "Published: %s", message.data.c_str());
  }

  // Member variables
  rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
  rclcpp::TimerBase::SharedPtr timer_;
  int count_;
};

int main(int argc, char *argv[]) {
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<Talker>());
}

Update CMakeLists.txt

Open CMakeLists.txt and update it to build and install the talker node:

cmake_minimum_required(VERSION 3.8)
project(talker_listener_cpp)

# Enable strict compiler warnings for GCC or Clang
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# Find necessary dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

# Define the executable
add_executable(talker src/talker.cpp)

# Set include directories (for headers if applicable)
target_include_directories(talker PUBLIC
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:include>
)

# Set C++ standard requirements
target_compile_features(talker PUBLIC cxx_std_17)  # Require C++17

# Link dependencies
ament_target_dependencies(talker rclcpp std_msgs)

# Install the executable
install(
  TARGETS talker
  DESTINATION lib/${PROJECT_NAME}
)

# Enable linting and testing (optional, remove if unnecessary)
if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # Skip copyright and cpplint checks (adjust based on your repo setup)
  set(ament_cmake_copyright_FOUND TRUE)
  set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()

# Mark the package as complete
ament_package()

2d. Build and Run the Talker Node

From the root of your workspace:

cd ~/ros2_ws
colcon build --symlink-install

Terminal Output Example:

Starting >>> talker_listener_cpp                                                        
Finished <<< talker_listener_cpp [7.45s]                     

Summary: 1 packages finished [7.45s]

Source the newly built setup files:

source install/setup.bash

Now run the talker node:

ros2 run talker_listener talker

Expected Terminal Output:

[INFO] [1734787302.346931961] [talker_cpp]: Published: Hello from talker_cpp: 0
[INFO] [1734787303.346917077] [talker_cpp]: Published: Hello from talker_cpp: 1
[INFO] [1734787304.346914631] [talker_cpp]: Published: Hello from talker_cpp: 2

Leave this running, or open a new terminal for the next steps.


3. Create the Listener (Subscriber)

In the talker_listener package, create listener.cpp:

listener.cpp:

#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>

class Listener : public rclcpp::Node {
public:
  Listener() : Node("listener_cpp") {
    // Create subscription
    subscription_ = this->create_subscription<std_msgs::msg::String>(
        "talker", 10,
        std::bind(&Listener::listener_callback, this, std::placeholders::_1));
  }

private:
  void listener_callback(const std_msgs::msg::String::SharedPtr msg) {
    RCLCPP_INFO(this->get_logger(), "Received: %s", msg->data.c_str());
  }

  // Member variables
  rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};

int main(int argc, char *argv[]) {
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<Listener>());

  return 0;
}

Add Listener to CMakeLists.txt

Update CMakeLists.txt to include the listener node:

cmake_minimum_required(VERSION 3.8)
project(talker_listener_cpp)

# Enable strict compiler warnings for GCC or Clang
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# Find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

# Define talker executable
add_executable(talker src/talker.cpp)
target_include_directories(talker PUBLIC
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:include>)
target_compile_features(talker PUBLIC cxx_std_17)  # Require C++17
ament_target_dependencies(talker rclcpp std_msgs)

# Define listener executable
add_executable(listener src/listener.cpp)
target_include_directories(listener PUBLIC
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:include>)
target_compile_features(listener PUBLIC cxx_std_17)  # Require C++17
ament_target_dependencies(listener rclcpp std_msgs)

# Install executables
install(TARGETS
  talker
  listener
  DESTINATION lib/${PROJECT_NAME})

# Enable linting and testing (optional)
if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # Skip copyright and cpplint checks (adjust as needed)
  set(ament_cmake_copyright_FOUND TRUE)
  set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()

# Mark package complete
ament_package()

Rebuild the package:

cd ~/ros2_ws
colcon build --symlink-install
source install/setup.bash

Terminal Output Example:

Starting >>> talker_listener_cpp                                                          
Finished <<< talker_listener_cpp [13.2s]                       

Summary: 2 packages finished [13.2s]

3a. Run the Listener Node

In a new terminal (with the workspace sourced):

ros2 run talker_listener_cpp listener

Expected Terminal Output:

[INFO] [1734793512.707757005] [listener_cpp]: Received: Hello from talker_cpp: 1
[INFO] [1734793513.707891775] [listener_cpp]: Received: Hello from talker_cpp: 2
[INFO] [1734793514.707862016] [listener_cpp]: Received: Hello from talker_cpp: 3

4. ROS 2 CLI Tools

You can verify your running nodes and topics using the ROS 2 CLI:

List active nodes:

ros2 node list

Expected Output:

/listener_cpp
/talker_cpp

List available topics:

ros2 topic list

Expected Output:

/parameter_events
/rosout
/talker

Echo messages from the talker topic (in another terminal):

ros2 topic echo /talker

Expected Output:

data: 'Hello from talker_cpp: 1'
---
data: 'Hello from talker_cpp: 2'
---

These tools help verify that nodes and topics are working as expected.


5. Summary

In this lesson, we:

Complete codeComplete code: talker_listener_cpp on Edreate GitHub

This fundamental setup of publishers and subscribers forms the building block for more complex robotic applications in ROS 2.

Previous Lesson Next Lesson