How to Create a ROS 2 C++ Subscriber – Iron

subscribers-reading-newspaper-park

In this tutorial, we will go over how to create a C++ subscriber for ROS 2.

In ROS 2 (Robot Operating System 2), a C++ subscriber is a program (written in C++) that listens for messages being published on a specific topic.

Topics in ROS 2 are channels of communication named according to the type of information they carry, such as “/robot/speed” for speed information or “/camera/image” for vision information. Each subscriber in ROS 2 declares its interest in a particular topic and is programmed to react or process the messages received on that topic.

The official instructions for creating a subscriber are here, but I will walk you through the entire process, step by step.

We will be following the ROS 2 C++ Style Guide.

Let’s get started!

Prerequisites

Directions

Open a terminal, and type these commands to open VS Code.

cd ~/ros2_ws
code .

Write the Code

Go back to the Explorer (Ctrl + Shift + E).

Right-click on the src folder to create a new file called “minimal_cpp_subscriber.cpp”.

Type the following code inside minimal_cpp_subscriber.cpp:

/**
 * @file minimal_cpp_subscriber.cpp
 * @brief Demonstrates subscribing to string messages on a ROS 2 topic.
 *
 * Description: Demonstrates the basics of subscribing to messages within the ROS 2 framework. 
 * The core functionality of this subscriber is to display output to the terminal window
 * when a message is received over a topic.
 * 
 * -------
 * Subscription Topics:
 *   String message
 *   /topic_cpp - std_msgs/String
 * -------
 * Publishing Topics:
 *   None
 * -------
 * @author Addison Sears-Collins
 * @date 2024-02-15
 */

#include "rclcpp/rclcpp.hpp" // ROS 2 C++ client library for node creation and management
#include "std_msgs/msg/string.hpp" // Standard message type for string messages
using std::placeholders::_1; // Create a placeholder for the first argument of the function

/**
 * @class MinimalSubscriber
 * @brief Defines a minimal ROS 2 subscriber node.
 *
 * This class inherits from rclcpp::Node and demonstrates creating a subscriber and
 * subscribing to messages.
 */
class MinimalSubscriber : public rclcpp::Node
{
public:
    /**
     * @brief Constructs a MinimalSubscriber node.
     *
     * Sets up a subscriber for 'std_msgs::msg::String' messages on the "topic_cpp" topic.     * 
     */
    MinimalSubscriber() : Node("minimal_subscriber")
    {
        // Create a subscriber object for listening to string messages on 
        // the "topic_cpp" topic with a queue size of 10.
        subscriber_ = create_subscription<std_msgs::msg::String>
        (
            "/topic_cpp", 
            10, 
            std::bind(
                &MinimalSubscriber::topicCallback, 
                this, 
                _1
            )
        );	
    }

    /**
     * @brief This function runs every time a message is received on the topic.
     *
     * This is the callback function of the subscriber. It publishes a string message
     * every time a message is received on the topic.
     * 
     * @param msg The string message received on the topic
     * @return Void.
     */
    void topicCallback(const std_msgs::msg::String &msg) const
    {
        // Write a message every time a new message is received on the topic.
        RCLCPP_INFO_STREAM(get_logger(), "I heard: " << msg.data.c_str());

    }
	
private:
    // Member variables.
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscriber_; // The subscriber object.
};

/**
 * @brief Main function.
 *
 * Initializes the ROS 2 system and runs the minimal_subscriber node. It keeps the node
 * alive until it is manually terminated.
 */
int main(int argc, char * argv[])
{

  // Initialize ROS 2.
  rclcpp::init(argc, argv); 
  
  // Create an instance of the MinimalSubscriber node and keep it running.
  auto minimal_subscriber_node = std::make_shared<MinimalSubscriber>();
  rclcpp::spin(minimal_subscriber_node);

  // Shutdown ROS 2 upon node termination.
  rclcpp::shutdown(); 

  // End of program.
  return 0; 
}

Configure CMakeLists.txt

Now we need to modify the CMakeLists.txt file inside the package so that the ROS 2 system will be able to find the cost we just wrote.

Open up the CMakeLists.txt file that is inside the package.

Make it look like this:

cmake_minimum_required(VERSION 3.8)
project(cobot_arm_examples)

# Check if the compiler being used is GNU's C++ compiler (g++) or Clang.
# Add compiler flags for all targets that will be defined later in the 
# CMakeLists file. These flags enable extra warnings to help catch
# potential issues in the code.
# Add options to the compilation process
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

# Locate and configure packages required by the project.
find_package(ament_cmake REQUIRED)
find_package(ament_cmake_python REQUIRED)
find_package(rclcpp REQUIRED)
find_package(rclpy REQUIRED)
find_package(std_msgs REQUIRED)

# Define a CMake variable named dependencies that lists all
# ROS 2 packages and other dependencies the project requires.
set(dependencies
  rclcpp
  std_msgs
)

# Add the specified directories to the list of paths that the compiler
# uses to search for header files. This is important for C++
# projects where you have custom header files that are not located
# in the standard system include paths.
include_directories(
  include
)

# Tells CMake to create an executable target named minimal_cpp_publisher
# from the source file src/minimal_cpp_publisher.cpp. Also make sure CMake
# knows about the program's dependencies.
add_executable(minimal_cpp_publisher src/minimal_cpp_publisher.cpp)
ament_target_dependencies(minimal_cpp_publisher ${dependencies})

add_executable(minimal_cpp_subscriber src/minimal_cpp_subscriber.cpp)
ament_target_dependencies(minimal_cpp_subscriber ${dependencies})

# Copy necessary files to designated locations in the project
install (
  DIRECTORY cobot_arm_examples scripts
  DESTINATION share/${PROJECT_NAME}
)

install(
  DIRECTORY include/
  DESTINATION include
)

# Install cpp executables
install(
  TARGETS
  minimal_cpp_publisher
  minimal_cpp_subscriber
  DESTINATION lib/${PROJECT_NAME}
)

# Install Python modules for import
ament_python_install_package(${PROJECT_NAME})

# Install Python executables
install(
  PROGRAMS
  scripts/minimal_py_publisher.py
  scripts/minimal_py_subscriber.py
  #scripts/example3.py
  #scripts/example4.py
  #scripts/example5.py
  #scripts/example6.py
  #scripts/example7.py
  DESTINATION lib/${PROJECT_NAME}
)

# Automates the process of setting up linting for the package, which
# is the process of running tools that analyze the code for potential
# errors, style issues, and other discrepancies that do not adhere to
# specified coding standards or best practices.
if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  # the following line skips the linter which checks for copyrights
  # comment the line when a copyright and license is added to all source files
  set(ament_cmake_copyright_FOUND TRUE)
  # the following line skips cpplint (only works in a git repo)
  # comment the line when this package is in a git repo and when
  # a copyright and license is added to all source files
  set(ament_cmake_cpplint_FOUND TRUE)
  ament_lint_auto_find_test_dependencies()
endif()

# Used to export include directories of a package so that they can be easily
# included by other packages that depend on this package.
ament_export_include_directories(include)

# Generate and install all the necessary CMake and environment hooks that 
# allow other packages to find and use this package.
ament_package()

Configure package.xml

Now we need to configure the package.xml file.

Open the package.xml file, and make sure it looks like this:

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
  <name>cobot_arm_examples</name>
  <version>0.0.0</version>
  <description>Basic examples demonstrating ROS 2</description>
  <maintainer email="automaticaddison@example.com">Addison Sears-Collins</maintainer>
  <license>Apache-2.0</license>

  <!--Specify build tools that are needed to compile the package-->
  <buildtool_depend>ament_cmake</buildtool_depend>
  <buildtool_depend>ament_cmake_python</buildtool_depend>

  <!--Declares package dependencies that are required for building the package-->
  <depend>rclcpp</depend>
  <depend>rclpy</depend>
  <depend>std_msgs</depend>

  <!--Specifies dependencies that are only needed for testing the package-->
  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>

  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

Build the Workspace

cd ~/ros2_ws
colcon build
source ~/.bashrc

Run the Nodes

First run your publisher node.

ros2 run cobot_arm_examples minimal_cpp_publisher 
23-run-cpp-publisher-1
24-topic-echo-cpp-publisher-1

Now run your subscriber node.

ros2 run cobot_arm_examples minimal_cpp_subscriber
27_minimal_cpp_subscriber

An Important Notes on Subscribers and Publishers

In the example above, we published a string message to a topic named /topic_cpp using a C++ node, and we subscribed to that topic using a C++node. 

ROS 2 is language agnostic, so we could have also used a Python node to the publishing and a C++ node to do the subscribing, and vice versa.

That’s it for now. Keep building!