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.
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.
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. Theament_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++.
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
:
touch ~/ros2_ws/src/tutorial_pkg/src/my_first_node.cpp
Open the file in your favorite text editor and paste the following code:
#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
-
Add the header file for the basic ROS 2 library.
#include "rclcpp/rclcpp.hpp"
-
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
-
In the public constructor create a node named
my_node
and display the informationNode started!
. In ROS 2, logging functions likeRCLCPP_INFO
are preferred overprintf
orcout
because they offer greater flexibility and support different log levels such asINFO
,WARN
,ERROR
, andDEBUG
.public:
MyNode() : Node("my_node") {
RCLCPP_INFO(get_logger(), "Node started!");
} -
In the
main
function first we need to initialize the underlying ROS 2 communication layer.rclcpp::init(argc, argv);
-
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>();
-
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);
-
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:
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
-
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) -
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() -
If the compiler is
GCC
orClang
, 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() -
These lines find the required dependencies for the package:
ament_cmake
andrclcpp
.find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED) -
This adds an executable named
my_first_node
based on the source filesrc/my_first_node.cpp
. Then is also specifies the dependency of the executable onrclcpp
using theament_target_dependencies
function.add_executable(my_first_node src/my_first_node.cpp)
ament_target_dependencies(my_first_node rclcpp) -
This specifies the installation destination for the
my_first_node
executable.install(TARGETS
my_first_node
DESTINATION lib/${PROJECT_NAME}
) -
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:
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:
cd ~/ros2_ws
colcon build --packages-select tutorial_pkg
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.
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:
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:
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
#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
- 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"
- 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;
- We declare a private subscriber, which will be initialized inside the constructor.
rclcpp::Subscription<sensor_msgs::msg::Image>::SharedPtr subscriber_;
- 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 ofMyNode
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.
- 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)
{
...
}
- Creating variable and iterating through each pixel value in the
data
field of theimage
message and adding it to thesum
.
long long sum = 0;
for (uint8_t value : image->data)
{
sum += value;
}
- 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:
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.
- ROSbot
- Gazebo simulator
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.
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.
Running the simulator is very easy thanks to the created aliases. If you haven't done so already check configure your environment.
ROSBOT_SIM
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.
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. Useros2 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
.
#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
- Add header file to obtain new message type
UInt8
.
#include "std_msgs/msg/u_int8.hpp"
- Declare publisher object:
rclcpp::Publisher<std_msgs::msg::UInt8>::SharedPtr publisher_;
- 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 streamsrclcpp::SensorDataQoS()
.
publisher_ = create_publisher<std_msgs::msg::UInt8>("/brightness", rclcpp::SensorDataQoS());
- 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.
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.
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. Useros2 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 floatgeometry_msgs
- for handling information regarding object position, orientation, velocity or acceleration, also for defining points or polygonsnav_msgs
- for handling maps or robot localization datasensor_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 positionssensor_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. 📧