It is not until you work on an important project that you realize the importance of small things in programming. For me, it is the header file in C++. “Why not declare the variables and functions in the .cpp file itself?” I kept asking myself which I now realize is stupid. Now, those who have read my past blogs might have realized that I work on a project involving automated mobility. An automated vehicle (not to be confused with an autonomous vehicle) contains numerous control modules for the sensors, actuators, path algorithms, etc. There is a good possibility that many people work on the same module at a time. Now imagine declaring all those functions and variables in a single source file. Either those people will quit the project after having a look at the code or switch to Python permanently. Even Github would stop you from pushing such chaotic code on its server!
Having a separate header and source file in such a case helps understand the structure of the project. A header file includes the declaration of the entities whereas the source file is where we define them. So when you see some function void activate_autopilot();
in the header file, you know it returns a null value and takes in no argument. Such a programming practice provides a better project structure, makes parallel programming convenient, and also encourages code usability when you are developing a shared object instead of an executable. Creating a header file and importing it in the source code using #include would have been the ideal approach if the code was strictly dependent only on C++ modules. As with any other robotics project in current times, my current project also uses the ROS framework for developing code for sensors and actuators of the vehicle.
Implementing such a concept in a ROS package is not trivial. Because a traditional ROS package has multiple dependencies, simply creating a header file in an “include” folder would not work. Additionally, this idea of structuring your project properly also comes in handy when multiple machines are involved and need to communicate with each other for a synchronized work environment.
In today’s blog, we will not take up a complicated project with tens of functions, rather we will create a simple listener node using a header file, thereby avoiding the declaration and definition of entities in the same file.
Table of Contents
Creating the header file
As it is obvious, the first step is to declare the identifiers, functions, etc. in a header file in the “include” folder of your ROS package. This folder should already have been generated when the ROS package was created.
#include <string>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
class MyNode : public rclcpp::Node
{
public:
MyNode();
private:
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr my_sub_;
void listener_callback(const std_msgs::msg::String::SharedPtr msg);
};
What’s happening here? Well, for starters, we include the necessary dependencies that are needed for our project. string
is an in-built module that is used when working with text. rclcpp.hpp
is the core of ROS2. It’s like the main library for building nodes. Finally, std_msgs/msg/string.hpp
is used for dealing with messages that contain strings.
Next comes the significant part. We created a public class called MyNode
. In it, we declared a ROS subscriber of the name my_sub_
that handles ROS topics of the type String
. Ultimately, the callback function listener_callback
is declared to be capable of receiving topics of the data type handled by the corresponding subscriber.
The Source file
After the declaration is completed, it is now time to define those terms. In the source file, we only need to include the header file we created above. There is no need to include the dependencies again in here.
#include "ros_cpp_test/my_node.hpp"
MyNode::MyNode():Node("my_custom_node")
{
my_sub_ = this->create_subscription<std_msgs::msg::String>("/chatter",10, std::bind(&MyNode::listener_callback, this, std::placeholders::_1));
}
void MyNode::listener_callback(const std_msgs::msg::String::SharedPtr msg)
{
RCLCPP_INFO(this->get_logger(), "I heard: %s", msg->data.c_str());
}
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MyNode>());
rclcpp::shutdown();
return 0;
}
In the constructor class that was declared earlier, we defined my_sub_
to create a subscription for the topic “/chatter”. The queue size for the incoming messages is set to 10. Following, the callback listener_callback
is bound to this subscriber using the std::bind function. This leads up to defining the callback. In the function, we simply read the incoming message and log it using the ROS logger function RCLCPP_INFO
. The content of the message topic is extracted and converted to string type using msg->data.c_str(). Ultimately, as a traditional C++ practice, we create the main function in which the node is started and the function returns 0.
We Are Not Done Yet!
ROS packages use CMake to instruct the compiler on how to build a ROS package. It specifies dependencies, compiler settings, and where to find your source and include files. This automated the build and compile processes.
...
include_directories(include)
install(
DIRECTORY include/
DESTINATION include/${PROJECT_NAME}
)
add_executable(cpp_listener src/my_node.cpp)
ament_target_dependencies(cpp_listener rclcpp std_msgs)
install(TARGETS
cpp_listener
DESTINATION lib/$(PROJECT_NAME))
install(TARGETS
cpp_listener
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION lib/${PROJECT_NAME}
)
...
Test Your Code
Now that the project configuration is defined in CMakeLists.txt, compile your ROS package using colcon build
. In one terminal, start the default talker node using ros2 run demo_nodes_cpp talker
. In a separate terminal, start your cpp_listener
node. If the compile process runs without any errors, your terminal should display the topics that are received.
Summary
In this blog, we understood why header files matter in programming and how to set them up for a simple ROS package. It emphasizes the importance of proper code organization by separating declarations from definitions into header and source files respectively. We started by declaring the necessary functions and identifiers used in our small project. We then created the source file, where the class, its callback, and the main function are defined.
To complete the project, the significance of the CMakeLists.txt file is highlighted. This file serves as the build system’s configuration, instructing the compiler on how to process the code.
By this structured approach, one can enhance code readability, maintainability, and reusability, which are crucial aspects of large-scale projects.