Creating Publisher and Subscriber Nodes in ROS 2 with C++
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:
- Understand what a ROS 2 package is.
- Learn about the purpose of a publisher and a subscriber.
- Explore how to use ROS 2 CLI tools to interact with nodes and topics.
- Implement a simple talker (publisher) and listener (subscriber) node in the talker_listener package.
Complete code: Complete 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:
- ROS 2 Humble or later installed.
colcon
build tool installed.- C++ compiler (e.g., g++) installed.
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
:
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:
- Created a ROS 2 package using
ament_cmake
. - Implemented a talker node (publisher) and a listener node (subscriber) in C++.
- Verified their functionality using ROS 2 CLI tools such as
ros2 node list
andros2 topic echo
.
Complete code: Complete 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.