Skip to main content

Creating Nodes - Messages

In this chapter, you'll make your first ROS 2 program. You'll apply what you've learned in the previous chapter about starting the camera. You'll start by creating a workspace, then make your first node, and discover how to build and run it. Your initial code will teach you how to send information between nodes using messages, which involves creating something called a publisher and a subscriber.

Prerequisites

This tutorial is dedicated to the ROSbot XL. However, most of the information and commands are also compatible with ROSbot 2R/2 PRO, and most changes are simple and come down to launching the appropriate container, depending on your hardware configuration. It is also possible to solve the tutorial using the Gazebo simulation.

info

If you don't already own a ROSbot, you can get one from the Husarion store.

The tutorials are designed to flow sequentially and are best followed in order. If you're looking to quickly assess the content of a specific tutorial, refer to the ROS 2 Tutorials repository, which contains the completed outcomes.

Workspace setup

To begin developing your own nodes in ROS 2, you need to do some workspace configuration first. A workspace is a place where all your source files, libraries, and compiled nodes will be stored.

First, you need to create a folder where your workspace will be located. It is recommended to add _ws to the end of the workspace name. All of the packages with source files should be stored inside the workspace in the src folder. Let's create our workspace named ros2_ws and src inside it.

husarion@husarion:~$
mkdir -p ~/ros2_ws/src

Creating a new package

In ROS 2, nodes are organized into packages, so in order to create a node, you need to create a package. Packages are created using the ros2 pkg create command and it must be executed in the src folder of your workspace. The syntax of ros2 pkg create is:

ros2 pkg create package_name --build-type ament_cmake --dependencies <dependencies>
  • package_name is the desired package name.
  • --build-type ament_cmake: option is used in the ROS 2 to specify the build system type for the package. The ament_cmake build system is the default build system in ROS 2 and is based on the ament build system. It is a meta-build system that leverages CMake as the underlying build tool.
  • --dependencies <dependencies> -- is an optional argument that contains the names of packages that are used by the newly created package.

For our tutorial, we will create a package named tutorial_pkg that depends on the package rclcpp. Package rclcpp is the ROS 2 library for C++.

husarion@husarion:~$
cd ~/ros2_ws/src
ros2 pkg create tutorial_pkg --build-type ament_cmake --dependencies rclcpp

After running this command, you will create a folder named tutorial_pkg and some files in it. Your workspace file structure should now look like this:

ros2_ws/
└── src/
└── tutorial_pkg/
├── CMakeLists.txt
├── include/
│ └── tutorial_pkg
├── package.xml
└── src/

The basic package contains:

  • CMakeLists.txt: This is the build instructions for your nodes. You need to edit this file if you want to compile any node. We will do it later.
  • include: This is folder where you put header files. Header files contain declarations of functions, classes, or other code elements that are used in your source code but are defined in other source files. They act as a way for your code to communicate with other parts of your program.
  • package.xml: This file contains package metadata such as author, description, version, or required packages. The package can be built without changing this file, but you should adjust it if you want to distribute your package to others.
  • src: This is folder where you put the source code files for your project. It typically contains the actual code files you write for your software, such as C++ or Python source code files.

Write your first node

Now you have a workspace created and your own package inside that workspace. You are ready to create your first ROS node.

Code of your first node

Let’s create a C++ file for your node and name it my_first_node.cpp. Place it in the src folder inside tutorial_pkg:

husarion@husarion:~$
touch ~/ros2_ws/src/tutorial_pkg/src/my_first_node.cpp

Open the file in your favorite text editor and paste the following code:

~/ros2_ws/src/tutorial_pkg/src/my_first_node.cpp
#include "rclcpp/rclcpp.hpp"

class MyNode : public rclcpp::Node
{
public:
MyNode() : Node("my_node") {
RCLCPP_INFO(get_logger(), "Node started!");
}
private:
};

int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<MyNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
The code explained
  1. Add the header file for the basic ROS 2 library.

    #include "rclcpp/rclcpp.hpp"
  2. The next line creates the node class MinimalPublisher by inheriting from rclcpp::Node. Every this in the code is referring to the node.

    class MyNode : public rclcpp::Node
  3. In the public constructor create a node named my_node and display the information Node started!. In ROS 2, logging functions like RCLCPP_INFO are preferred over printf or cout because they offer greater flexibility and support different log levels such as INFO, WARN, ERROR, and DEBUG.

    public:
    MyNode() : Node("my_node") {
    RCLCPP_INFO(get_logger(), "Node started!");
    }
  4. In the main function first we need to initialize the underlying ROS 2 communication layer.

    rclcpp::init(argc, argv);
  5. Create a node object. This object is used for interactions with the ROS 2 system, such as subscribing to topics or publishing messages.

    auto node = std::make_shared<MyNode>();
  6. The spin function starts the ROS 2 node, allowing it to handle callbacks, execute work, and respond to incoming messages. This call will block the main thread and continuously loop until the node is shut down.

    rclcpp::spin(node);
  7. This function shuts down the ROS 2 communication infrastructure, releasing all associated resources.

    rclcpp::shutdown()

Remember to save the C++ file.

Building configuration

Before you build the node, you need to edit the CMakeLists.txt file in the tutorial_pkg directory. Open the file in your favorite text editor and paste the following content:

~/ros2_ws/src/tutorial_pkg/CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(tutorial_pkg)

# Set C++ standard to C++14
if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 14)
endif()

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)

add_executable(my_first_node src/my_first_node.cpp)
ament_target_dependencies(my_first_node rclcpp)

install(TARGETS
my_first_node
DESTINATION lib/${PROJECT_NAME}
)

ament_package()
The code explained
  1. This sets the minimum required version of CMake and defines the project name as tutorial_pkg.

    cmake_minimum_required(VERSION 3.8)
    project(tutorial_pkg)
  2. This sets the C++ standard to C++14 if it is not already set.

    if(NOT CMAKE_CXX_STANDARD)
    set(CMAKE_CXX_STANDARD 14)
    endif()
  3. If the compiler is GCC or Clang, it adds additional compile options for enabling warnings.

    if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
    add_compile_options(-Wall -Wextra -Wpedantic)
    endif()
  4. These lines find the required dependencies for the package: ament_cmake and rclcpp.

    find_package(ament_cmake REQUIRED)
    find_package(rclcpp REQUIRED)
  5. This adds an executable named my_first_node based on the source file src/my_first_node.cpp. Then is also specifies the dependency of the executable on rclcpp using the ament_target_dependencies function.

    add_executable(my_first_node src/my_first_node.cpp)
    ament_target_dependencies(my_first_node rclcpp)
  6. This specifies the installation destination for the my_first_node executable.

    install(TARGETS
    my_first_node
    DESTINATION lib/${PROJECT_NAME}
    )
  7. This is a command provided by ament_cmake to build the package and set up necessary dependencies for ROS 2.

    ament_package()

Save the changes and close the editor. Now, you are ready to proceed with the ROS 2 build process for your workspace.

Build code

ROS2 use colcon tool to build your source code, so make sure colcon is correctly installed on your machine:

husarion@husarion:~$
sudo apt install python3-colcon-common-extensions

When the building configuration is finished, you can navigate to your workspace main directory and build your project using the colcon build command:

husarion@husarion:~/ros2_ws$
cd ~/ros2_ws
colcon build --packages-select tutorial_pkg
tip

Adding the --packages-select tutorial_pkg flag ensures that only the specified package, tutorial_pkg, will be built, which can speed up the building process. Omitting this flag will result in rebuilding all the packages inside the workspace, which may take more time.

After executing this command, you should see the build output similar to the one shown below and two new folders will be created inside your ros2_ws:

  • build: stores build binary files,
  • install: holds the final installable artifacts,
  • log: contains runtime logs.

image

note

Remember to build your package every time you make changes to your code.

Run node

Sourcing files

Before running your node, you need to source the appropriate environment setup files. Open a terminal and navigate to your workspace main directory, then run the following command to source the setup files:

husarion@husarion:~$
source ~/ros2_ws/install/setup.bash

These environment variables enable you to run your node from any directory. To avoid manually sourcing the setup files every time you open a new terminal, you can add the following line to your .bashrc file to automatically source your ROS workspace:

husarion@husarion:~$
echo 'source ~/ros2_ws/install/setup.bash' >> ~/.bashrc

Task 1 - Running the node

Run your node using the ros2 run command. Then use the ros2 node list command to examine the system and check if your node is visible.

Hint

In the previous chapter you learned the ros2 run and ros2 launch commands to run code. In this case it is necessary to execute the ros2 run command because we do not have a launch file created. As a reminder, running the ros2 run command looks like this.

ros2 run package_name executable_name [options]

After a successful startup, you should see the text Node started! and you should be able to see the new node named my_node using the ros2 node list command.

Creating a subscriber

As you can see, your first node is pretty boring. It does nothing but output one line of text to the terminal. Let's modify the previous code. We will use our camera to calculate the average brightness of the received image. To subscribe image from /image topic we will use the following code.

Open again the previous my_first_node.cpp

~/ros2_ws/src/tutorial_pkg/src/my_first_node.cpp
#include "rclcpp/rclcpp.hpp"
#include "sensor_msgs/msg/image.hpp"

using namespace std::placeholders;

class MyNode : public rclcpp::Node
{
public:
MyNode() : Node("my_node")
{
subscriber_ = create_subscription<sensor_msgs::msg::Image>(
"/image", rclcpp::SensorDataQoS(), std::bind(&MyNode::image_callback, this, _1));

RCLCPP_INFO(get_logger(), "Node started!");
}

private:
void image_callback(const sensor_msgs::msg::Image::SharedPtr image)
{
long long sum = 0;
for (uint8_t value : image->data)
{
sum += value;
}
int avg = sum / image->data.size();
RCLCPP_INFO(get_logger(), "Brightness: %d", avg);
}

rclcpp::Subscription<sensor_msgs::msg::Image>::SharedPtr subscriber_;
};

int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<MyNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
The code explained
  1. To process messages received from the camera, you need to include the header file for the Image message type. The message fields can be found here:
#include "sensor_msgs/msg/image.hpp"
  1. The statement using namespace std::placeholders; allows you to use placeholders (_1, _2, etc.) from the std::placeholders namespace without specifying the namespace each time.
using namespace std::placeholders;
  1. We declare a private subscriber, which will be initialized inside the constructor.
rclcpp::Subscription<sensor_msgs::msg::Image>::SharedPtr subscriber_;
  1. Defining a subscriber.
subscriber_ = create_subscription<sensor_msgs::msg::Image>(
"/image", rclcpp::SensorDataQoS(), std::bind(&MyNode::image_callback, this, _1));

It may seem complicated, so let's break the definitions down into components:

  • <sensor_msgs::msg::Image>: template specifies what type of messages the subscriber will listen to.
  • /image: name of topic to subscribe.
  • rclcpp::SensorDataQoS(): sending profile optimized for sensor data streams, where it is crucial to receive the most recent sensor data without missing any updates.
  • std::bind(&MyNode::image_callback, this, _1): this part is responsible for binding the callback function to the subscription. It creates a function object that will be called whenever a new message is received on the subscribed topic.
    • &MyNode::image_callback: pointer to the callback function of the specific class.
    • this: this is a pointer to the current instance of MyNode class.
    • _1: This represents a placeholder for the argument of the callback function (image_callback). It specifies that the first argument of the received message should be passed as an argument to the callback function.
  1. The callback function for processing the received image message. The argument is a shared pointer to the incoming message.
void image_callback(const sensor_msgs::msg::Image::SharedPtr image)
{
...
}
  1. Creating variable and iterating through each pixel value in the data field of the image message and adding it to the sum.
long long sum = 0;
for (uint8_t value : image->data)
{
sum += value;
}
  1. Calculating the average value by dividing the sum by the total number of pixels and printing the brightness value.
int avg = sum / image->data.size();
RCLCPP_INFO(get_logger(), "Brightness: %d", avg);

Building configuration

Before running the code, we need to find and link the new sensor_msgs dependency to the CMakeList.txt. Please add highlighted changes:

~/ros2_ws/src/tutorial_pkg/CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(tutorial_pkg)

# Set C++ standard to C++14
if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 14)
endif()

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(sensor_msgs REQUIRED)

add_executable(my_first_node src/my_first_node.cpp)
ament_target_dependencies(my_first_node rclcpp sensor_msgs)

install(TARGETS
my_first_node
DESTINATION lib/${PROJECT_NAME}
)

ament_package()

Build & run your code

Now you can build and run this node along with camera. To run the camera, run the RObot or Gazebo simulator as shown in the previous chapter.

Starting ROSbot camera

Running the camera on ROSbot is very simple thanks to the use of docker images. All you need to do is run the specified container.

husarion@husarion:~$
docker compose up -d microros <rosbot_service> <camera_service>

Depending on your ROSbot model and specific configuration you should use an appropriate name for your <rosbot_service> and <camera_service>. Check Husarion Docker to find suitable image.

Before we run our node, we need to find the camera topic name. To check your topics use ros2 topic list command or rqt_image_tool. When we have the camera topic name we can now run our node with the remapping topic.

husarion@husarion:~$
ros2 run tutorial_pkg my_first_node --ros-args -r /image:=<camera_topic>

<camera_topic> parameter depends on the type of camera you are using. Use ros2 topic list to find the camera topic.

After running this launcher in the terminal, you should see the brightness information.

Creating a publisher

Now let's modify the node to publish brightness value to a new topic instead of logging info into the terminal. For this purpose, we will use message type std_msgs/UInt8.

~/ros2_ws/src/tutorial_pkg/src/my_first_node.cpp
#include "rclcpp/rclcpp.hpp"
#include "sensor_msgs/msg/image.hpp"
#include "std_msgs/msg/u_int8.hpp"

class MyNode : public rclcpp::Node
{
public:
MyNode() : Node("my_node")
{
subscriber_ = create_subscription<sensor_msgs::msg::Image>(
"/image", rclcpp::SensorDataQoS(), std::bind(&MyNode::image_callback, this, _1));
publisher_ = create_publisher<std_msgs::msg::UInt8>("/brightness", rclcpp::SensorDataQoS());

RCLCPP_INFO(get_logger(), "Node started!");
}

private:
void image_callback(const sensor_msgs::msg::Image::SharedPtr image)
{
long long sum = 0;
for (uint8_t value : image->data)
{
sum += value;
}
int avg = sum / image->data.size();

std_msgs::msg::UInt8 brightness_msg;
brightness_msg.data = avg;
publisher_->publish(brightness_msg);
}

rclcpp::Subscription<sensor_msgs::msg::Image>::SharedPtr subscriber_;
rclcpp::Publisher<std_msgs::msg::UInt8>::SharedPtr publisher_;
};

int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<MyNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
The code explained
  1. Add header file to obtain new message type UInt8.
#include "std_msgs/msg/u_int8.hpp"
  1. Declare publisher object:
rclcpp::Publisher<std_msgs::msg::UInt8>::SharedPtr publisher_;
  1. Next, define a publisher on your system to publish a std_msgs::msg::UInt8 message named /brightness with QoS (Quality of Service) profile optimized for sensor data streams rclcpp::SensorDataQoS() .
publisher_ = create_publisher<std_msgs::msg::UInt8>("/brightness", rclcpp::SensorDataQoS());
  1. You also need to declare the type of message which will be published (brightness_msg), put some data into the message, and send it whenever we calculate a mean brightness.
std_msgs::msg::UInt8 brightness_msg;
brightness_msg.data = avg;
publisher_->publish(brightness_msg);

Building configuration

As before we should add the new std_msgs dependency to the CMakeList.txt. However, this is not necessary because std_msgs messages are automatically linked with the messages sensor_msgs. For didactic purposes, we add a new dependency.

~/ros2_ws/src/tutorial_pkg/CMakeLists.txt
cmake_minimum_required(VERSION 3.8)
project(tutorial_pkg)

# Set C++ standard to C++14
if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 14)
endif()

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(sensor_msgs REQUIRED)
find_package(std_msgs REQUIRED)

add_executable(my_first_node src/my_first_node.cpp)
ament_target_dependencies(my_first_node rclcpp sensor_msgs std_msgs)

install(TARGETS
my_first_node
DESTINATION lib/${PROJECT_NAME}
)

ament_package()

Build & run your code

To build and run your code. You can use the same command as before. Make sure your camera or simulator is working.

husarion@husarion:~$
ros2 run tutorial_pkg my_first_node --ros-args -r /image:=<camera_topic>

<camera_topic> parameter depends on the type of camera you are using. Use ros2 topic list to find the camera topic.

Task 2 - Examining topics

Use ros2 topic echo to read the brightness of the image from the /brightness topic.

Hint

As a reminder, you should type:

ros2 topic echo [topic_name]

Message types

ROS 2 packages, apart from the functionality itself, provide various types of messages. Some of the most common packages used are:

  • std_msgs - messages with basic data types like integer or float
  • geometry_msgs - for handling information regarding object position, orientation, velocity or acceleration, also for defining points or polygons
  • nav_msgs - for handling maps or robot localization data
  • sensor_msgs - for handling data from sensors

Each of these packages contains many types of messages, which have their uses in different applications. When creating nodes for mobile robots, you'll likely encounter some of these messages:

  • geometry_msgs/Twist - message for defining linear and angular object velocity. Could be also used for setting desired velocities.
  • nav_msgs/OccupancyGrid - message for handling flat occupancy grid based maps.
  • nav_msgs/Odometry - message for defining robot position and orientation based on odometry measurements.
  • nav_msgs/Path - message for defining robot path as a set of positions
  • sensor_msgs/JointState - it has fields for determining position, velocity and effort for a set of joints.
  • sensor_msgs/LaserScan - message for handling scans of planar laser scanners like RpLidar or Hokuyo.
  • sensor_msgs/Range - message for one dimensional distance measurement

It is also possible to implement user-defined messages if existing ones are not sufficient.

Summary

After completing this tutorial you should know how to:

  • create a workspace,
  • create your custom nodes, that can send and receive messages,
  • build code and configure building process,
  • run and examine the system.

In the next tutorial, we will continue this topic. You will learn about another method of communication which are services. See you there!


by Rafał Górecki, Husarion

Need help with this article or experiencing issues with software or hardware? 🤔

  • Feel free to share your thoughts and questions on our Community Forum. 💬
  • To contact service support, please use our dedicated Issue Form. 📝
  • Alternatively, you can also contact our support team directly at: support@husarion.com. 📧