ROS2 from the Ground Up: Part 3-A Hands-On Guide to Creating Custom Messages(.msg & .srv) and Turtlebot3 Service Node

Jegathesan Shanmugam
10 min readDec 24, 2022

--

Robots (AI generated Image)

Custom message definition files are a important part of the ROS ecosystem, as they allow you to define the custom data structure used to represent the information that can exchange between nodes based on your application’s specifications and design. This is useful if the ROS-provided built-in message types do not meet your requirements, or if you wish to use a specific data format that is not supported by the built-in message types.

These.msg .srvfiles are input to IDL (Interface Definition Language) generators, which generate code in a variety of programming languages, including C++, Python, and others. The generated code contains classes for serialising and deserializing message data, as well as access and modification methods for message fields. This code can then be utilised in a ROS 2 application to create and manipulate message data.

Understanding how these.msg .srvfiles are embedded in code and used for communication is critical.
Let’s get started!

Reference: ROS2 internal Interface Design

For now, let’s concentrate on rosidl_dds and rosidl_generator_cpp lib.
rosidl_generator_dds is an extension of the rosidl library that provides support for generating code for DDS communication protocol.
rosidl_generator_cpp is another extension of the rosidl library that provides support for generating C++ code for custom messages, services, and actions.

Here’s an example of a custom message creation,

mkdir ~/ros2_ws/src
# Create a custom_msg package
cd ~/ros2_ws/src && ros2 pkg create --build-type ament_cmake custom_msg --dependencies rclcpp std_msgs rosidl_default_generators
mkdir -p ~/ros2_ws/src/custom_msg/msg/
# Create a LogTf.msg
touch ~/ros2_ws/src/custom_msg/msg/touch LogTf.msg
string time_frame
string log_msg

This message definition file defines a message that has two fields: time_frame and log_msg. The time_frame field is of type string and can be used to capture the time when the message was generated or logged. The log_msg field is also of type string and can be used to store a log message, such as "hello_world".

Add the following lines to CMakeLists.txt,

# Add after BUILD_TESTING endif()
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/LogTf.msg"
)

Add the following lines to to package.xml,

# Add after <depend>std_msgs</depend> line
<depend>rosidl_default_generators</depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
# Compile and Run,
cd ~/ros2_ws
colcon build --packages-select custom_msg #Compilation
source install/setup.zsh

# The ros2 interface show command allows you to
# display information about the interfaces.
ros2 interface show custom_msg/msg/LogTf

Let us examine the compilation output and generated header files.

./install/custom_msg/include/custom_msg/msg/log_tf.hpp
./install/custom_msg/include/custom_msg/msg/log_tf.h
./install/custom_msg/include/custom_msg/msg/detail/log_tf__struct.h
./install/custom_msg/include/custom_msg/msg/detail/log_tf__rosidl_typesupport_fastrtps_c.h
./install/custom_msg/include/custom_msg/msg/detail/log_tf__type_support.cpp
./install/custom_msg/include/custom_msg/msg/detail/log_tf__type_support.c
./install/custom_msg/include/custom_msg/msg/detail/log_tf__struct.hpp
./install/custom_msg/include/custom_msg/msg/detail/log_tf__traits.hpp
./install/custom_msg/include/custom_msg/msg/detail/log_tf__builder.hpp
./install/custom_msg/include/custom_msg/msg/detail/log_tf__type_support.h
./install/custom_msg/include/custom_msg/msg/detail/log_tf__functions.h
./install/custom_msg/include/custom_msg/msg/detail/log_tf__rosidl_typesupport_introspection_c.h
./install/custom_msg/include/custom_msg/msg/detail/log_tf__functions.c
./install/custom_msg/include/custom_msg/msg/detail/log_tf__rosidl_typesupport_introspection_cpp.hpp
./install/custom_msg/include/custom_msg/msg/detail/log_tf__rosidl_typesupport_fastrtps_cpp.hpp
./install/custom_msg/include/custom_msg/msg/log_tf__rosidl_typesupport_connext_cpp.hpp
./install/custom_msg/include/custom_msg/msg/log_tf__rosidl_typesupport_connext_c.h

The files listed above were generated by the IDL generator. It is generated for both programming specific and DDS connext.

To use the custom_msg message in our ROS 2 application, We include the generated message header file in our source code and use the message class to create and manipulate message objects. For example:

cd ~/ros2_ws/src/
# Create a pub_sub_custom_msg package
ros2 pkg create pub_sub_custom_msg --build-type ament_cmake --dependencies rclcpp std_msgs custom_msg

# Create a publisher cpp node
touch ~/ros2_ws/src/pub_sub_custom_msg/src/pub.cpp

# Create a subscriber cpp node
touch ~/ros2_ws/src/pub_sub_custom_msg/src/sub.cpp
// pub.cpp
// C++ Header files
#include <chrono>
#include <functional>
#include <memory>
#include <string>
#include <ctime> // Change

// Import the rclcpp client library and std_msgs header
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

// Include our custom message
#include "custom_msg/msg/log_tf.hpp" // Change

using namespace std::chrono_literals;

// PublisherNode Composition
//We create a PublisherNode class that inherits from the rclcpp::Node
class PublisherNode: public rclcpp::Node
{
public:
PublisherNode()
: Node("publisher"), count_(0)
{
// Create a Publisher with String message type and queue size is 10
publisher_ = this->create_publisher<custom_msg::msg::LogTf>("string_msg", 10); // Change

//Create a timer with 500ms delay
timer_ = this->create_wall_timer(
500ms, std::bind(&PublisherNode::callback, this));
}

private:
void callback()
{
custom_msg::msg::LogTf message = custom_msg::msg::LogTf(); // Change
time_t now = time(0); // Change
message.time_frame = ctime(&now); // Change
message.log_msg = "Hello, world! " + std::to_string(count_++);

// Publish the message
publisher_->publish(message);
}

// Timer objects allow a node to perform a specific action at a specified rate or at a specific time in the future
rclcpp::TimerBase::SharedPtr timer_;

// Publisher
rclcpp::Publisher<custom_msg::msg::LogTf>::SharedPtr publisher_; // Change

// Variable to count number of message published
size_t count_;
};

int main(int argc, char * argv[])
{
// Initialize the ROS2 communication
rclcpp::init(argc, argv);

// Create a default single-threaded executor and spin the PublisherNode node
rclcpp::spin(std::make_shared<PublisherNode>());

// Shutdown the ROS2 communication
rclcpp::shutdown();
return 0;
}
// sub.cpp
// C++ Header files
#include <memory>

// Import the rclcpp client library and std_msgs header
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

// Include our custom message
#include "custom_msg/msg/log_tf.hpp" //change

// creation of function objects with placeholder arguments
using std::placeholders::_1;

// SubscriberNode Composition
//We create a SubscriberNode class that inherits from the rclcpp::Node
class SubscriberNode : public rclcpp::Node
{
public:
SubscriberNode()
: Node("subscriber")
{
// Create a subscriber with String message type and queue size is 10
subscription_ = this->create_subscription<custom_msg::msg::LogTf>(
"string_msg", 10, std::bind(&SubscriberNode::callback, this, _1)); //change
}

private:
// The moment the message available in the queue and
// the call back execute the print statement
void callback(const custom_msg::msg::LogTf::SharedPtr msg) const
{
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->time_frame.c_str()); //change
RCLCPP_INFO(this->get_logger(), "%s", msg->log_msg.c_str()); //change
}

// Subscriber
rclcpp::Subscription<custom_msg::msg::LogTf>::SharedPtr subscription_;
};

int main(int argc, char * argv[])
{
// Initialize the ROS2 communication
rclcpp::init(argc, argv);

// Create a default single-threaded executor and spin the SubscriberNode node
rclcpp::spin(std::make_shared<SubscriberNode>());

// Shutdown the ROS2 communication
rclcpp::shutdown();

return 0;
}

Let’s make a launch file, as we always do.

cd ~/ros2_ws/src/pub_sub_custom_msg/ && mkdir launch 
touch launch/pub_sub_launch.py
# pub_sub_launch.py
from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
return LaunchDescription([
Node(
package='pub_sub_custom_msg', # Package Name
executable='publisher', # Executable file
output='screen',
emulate_tty=True),

Node(
package='pub_sub_custom_msg', # Package Name
executable='subscriber', # Executable file
output='screen',
emulate_tty=True),
])

Update the CMakeLists.txt,

cmake_minimum_required(VERSION 3.5)
project(pub_sub_custom_msg)

# Default to C99
if(NOT CMAKE_C_STANDARD)
set(CMAKE_C_STANDARD 99)
endif()

# Default 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(std_msgs REQUIRED)
find_package(custom_msg REQUIRED)

if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
# the following line skips the linter which checks for copyrights
# uncomment the line when a copyright and license is not present in all source files
#set(ament_cmake_copyright_FOUND TRUE)
# the following line skips cpplint (only works in a git repo)
# uncomment the line when this package is not in a git repo
#set(ament_cmake_cpplint_FOUND TRUE)
ament_lint_auto_find_test_dependencies()
endif()

# Compilation and Installtion
add_executable(publisher src/pub.cpp)
# Change
ament_target_dependencies(publisher rclcpp custom_msg)
# change
add_executable(subscriber src/sub.cpp)
ament_target_dependencies(subscriber rclcpp custom_msg)

install(TARGETS
publisher
subscriber
DESTINATION lib/${PROJECT_NAME}
)

install(DIRECTORY
launch
DESTINATION share/${PROJECT_NAME}/
)

ament_package()

Lets compile and Run,

cd ~/ros2_ws/
colcon build --packages-select pub_sub
source install/setup.zsh
ros2 launch pub_sub_custom_msg pub_sub_launch.py

# Output
[INFO] [launch]: Default logging verbosity is set to INFO
[INFO] [publisher-1]: process started with pid [404398]
[INFO] [subscriber-2]: process started with pid [404400]
[subscriber-2] [INFO] [1671639479.441939131] [subscriber]: I heard: 'Wed Dec 21 21:47:59 2022
[subscriber-2] '
[subscriber-2] [INFO] [1671639479.442198963] [subscriber]: Hello, world! 0
[subscriber-2] [INFO] [1671639479.941902411] [subscriber]: I heard: 'Wed Dec 21 21:47:59 2022
[subscriber-2] '
[subscriber-2] [INFO] [1671639479.942009921] [subscriber]: Hello, world! 1
[subscriber-2] [INFO] [1671639480.441938914] [subscriber]: I heard: 'Wed Dec 21 21:48:00 2022

Service:

Services use a call-response model for communication. In this model, a node that wants to request a specific action or operation from another node (the “client”) sends a request message to the service, and the node that provides the service (the “server”) responds with a response message. The client and server need to know about each other in order to communicate.

To control the Turtlebot3, we will create a service server node that accepts start and stop requests. To do so, we will define a service in a ROS2 package and implement a callback function. We will create a service client node that will send a request and then wait for a response, whether it is successful or not.

Let’s get the Turtlebot3 simulation workspace up and running.

# Note: This steps are testes on Ubuntu 20.04
# Install Gazebo
sudo apt-get install ros-foxy-gazebo-*

# Install Cartographer
sudo apt install ros-foxy-cartographer ros-foxy-cartographer-ros

# Install Navigation2
sudo apt install ros-foxy-navigation2 ros-foxy-nav2-bringup

# Compile turtlebot3_simulations
source /opt/ros/foxy/setup.bash #Incase if you have multiple ROS environment
mkdir -p ~/turtlebot3_ws/src/
cd ~/turtlebot3_ws/src/
git clone -b foxy-devel https://github.com/ROBOTIS-GIT/turtlebot3_simulations.git
cd ~/turtlebot3_ws/
colcon build --symlink-install

# Launch the Burger model and test with
# Terminal 1
source install/setup.bash
export TURTLEBOT3_MODEL=burger
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py

# Terminal 2
source /opt/ros/foxy/setup.bash #Incase if you have multiple ROS environment
cd ~/turtlebot3_ws/
source install/setup.bash
export TURTLEBOT3_MODEL=burger
ros2 run turtlebot3_teleop teleop_keyboard

# Refere here:
# https://emanual.robotis.com/docs/en/platform/turtlebot3/overview/
Output of ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py

We begin by creating our custom service file.

cd ~/ros2_ws/src
# Create custom service package
ros2 pkg create --build-type ament_cmake custom_srv --dependencies rclcpp std_msgs rosidl_default_generators
# Create a custom service
mkdir -p ~/ros2_ws/src/custom_srv/srv/
touch ~/ros2_ws/src/custom_srv/srv/StartStop.srv
string start_stop 
---
bool success

The start_stop message used to control a Turtlebot3 by starting or stopping it as needed. The function or service should return True if the operation was successful, and False if it was not.

cd ~/ros2_ws/
# Compile
colcon build --packages-select custom_srv
source install/setup.bash

# Show the interface info
ros2 interface show custom_srv/srv/StartStop

When you create a custom msg or service follow the convention that is defined in rosidl.
rosidl/rosidl_adapter/rosidl_adapter/parser.py
# relaxed patterns used for compatibility with ROS 1 messages
# VALID_MESSAGE_NAME_PATTERN = re.compile(‘^[A-Za-z][A-Za-z0–9]*$’)

Let us now incorporate the custom service message into our service server and client application.

cd ~/ros2_ws/src/
# Create turtlebot_srv package
ros2 pkg create turtlebot_srv --build-type ament_cmake --dependencies rclcpp std_msgs custom_srv #Note, we added custom_srv as dep
cd ~/ros2_ws/src/turtlebot_srv/src

# Create service server and client file
touch service_server.cpp

# Create a launch file
mkdir -p ~/ros2_ws/src/turtlebot_srv/launch/
touch ~/ros2_ws/src/turtlebot_srv/launch/service_server_client_launch.py
// service_server.cpp
// C++ Header files
#include <iostream>
#include <memory>

// Import the rclcpp client library and message headers
#include "rclcpp/rclcpp.hpp"
#include "std_srvs/srv/set_bool.hpp"
#include "geometry_msgs/msg/twist.hpp"

// Include our custom service
#include "custom_srv/srv/start_stop.hpp"

using SetBool = std_srvs::srv::SetBool;
using std::placeholders::_1;
using std::placeholders::_2;

// ServerNode Composition
//We create a ServerNode class that inherits from the rclcpp::Node
class ServerNode : public rclcpp::Node
{
public:
ServerNode()
: Node("start_stop_server")
{
// Create a service with custom request and response
service_ = this->create_service<custom_srv::srv::StartStop>("start_stop_srv", std::bind(&ServerNode::callback, this, _1, _2));

// Create a Publisher with geometry_msgs::msg::Twist message type and queue size is 10
publisher_ = this->create_publisher<geometry_msgs::msg::Twist>("cmd_vel", 10);

}

private:
rclcpp::Service<custom_srv::srv::StartStop>::SharedPtr service_;
rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr publisher_;

void callback(
const std::shared_ptr<custom_srv::srv::StartStop::Request> request,
const std::shared_ptr<custom_srv::srv::StartStop::Response> response)
{

geometry_msgs::msg::Twist message = geometry_msgs::msg::Twist();

std::cout << "Requested Data: " << request->start_stop << std::endl;

// If request is start, send velocities to move the robot
if (request->start_stop == "start")
{
message.linear.x = 0.2;
message.angular.z = -0.2;
publisher_->publish(message);

// Success variable to true
response->success = true;
}

// If request is start, send velocities to stop the robot
if (request->start_stop == "stop")
{
message.linear.x = 0.0;
message.angular.z = 0.0;
publisher_->publish(message);

// Success variable to True
response->success = true;
}
}
};

int main(int argc, char * argv[])
{
// Initialize the ROS2 communication
rclcpp::init(argc, argv);

// Create a default single-threaded executor and spin the PublisherNode node
rclcpp::spin(std::make_shared<ServerNode>());

// Shutdown the ROS2 communication
rclcpp::shutdown();

return 0;
}

Update service_server_client_launch.py file,

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
return LaunchDescription([
Node(
package='turtlebot_srv', # Package Name
executable='server', # Executable file
output='screen',
emulate_tty=True),
])

Update the CMakeLists.txt,

cmake_minimum_required(VERSION 3.8)
project(turtlebot_srv)

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)
find_package(custom_srv REQUIRED)
find_package(geometry_msgs REQUIRED)

if(BUILD_TESTING)
find_package(ament_lint_auto REQUIRED)
# the following line skips the linter which checks for copyrights
# uncomment the line when a copyright and license is not present in all source files
#set(ament_cmake_copyright_FOUND TRUE)
# the following line skips cpplint (only works in a git repo)
# uncomment the line when this package is not in a git repo
#set(ament_cmake_cpplint_FOUND TRUE)
ament_lint_auto_find_test_dependencies()
endif()

add_executable(server src/service_server.cpp)
ament_target_dependencies(server rclcpp geometry_msgs custom_srv)

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

install(DIRECTORY
launch
DESTINATION share/${PROJECT_NAME}/
)

ament_package()

Let’s compile and run, shall we?

cd ~/ros2_ws/
colcon build --packages-select turtlebot_srv #Make sure you compiled custom_srv
source install/setup.zsh

# Terminal 1
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py # Launch Turtlebot3


# Terminal 2 - Run the server
ros2 launch turtlebot_srv service_server_client_launch.py

# Output
[INFO] [launch]: All log files can be found below /home/nullbyte/.ros/log/2022-12-22-19-14-05-892159-edgeai-139978
[INFO] [launch]: Default logging verbosity is set to INFO
[INFO] [server-1]: process started with pid [139986]

Now the service server waiting for the client request. We can send the request from command line as well.

# Terminal 1
export TURTLEBOT3_MODEL=burger
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py

# Terminal 2
ros2 launch turtlebot_srv service_server_client_launch.py

# Terminal 3
ros2 service call /start_stop_srv custom_srv/srv/StartStop "{start_stop: 'start'}"
# You should see Robot moving

ros2 service call /start_stop_srv custom_srv/srv/StartStop "{start_stop: 'stop'}"
# Stop the robot

Now we will do it programmatically.

touch ~/ros2_ws/src/turtlebot_srv/src/service_client.cpp
// service_client.cpp
// C++ Header files
#include <iostream>
#include <memory>
#include <chrono>

// Import the rclcpp client library and message headers
#include "rclcpp/rclcpp.hpp"
#include "std_srvs/srv/set_bool.hpp"

// Include our custom service
#include "custom_srv/srv/start_stop.hpp"

using SetBool = std_srvs::srv::SetBool;
using std::placeholders::_1;
using std::placeholders::_2;
using namespace std::chrono_literals;

int main(int argc, char * argv[])
{

std::string input;

// Initialize the ROS2 communication
rclcpp::init(argc, argv);

// Create a Node
std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("start_stop_client");

// Create a client with custom request and response
rclcpp::Client<custom_srv::srv::StartStop>::SharedPtr client =
node->create_client<custom_srv::srv::StartStop>("start_stop_srv");

auto request = std::make_shared<custom_srv::srv::StartStop::Request>();

// Wait for service activation
while (!client->wait_for_service(1s)) {
if (!rclcpp::ok()) {
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");
return 0;
}
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");
}

// Update the request
request->start_stop = argv[1];

// Request server
auto result = client->async_send_request(request);

// Wait result from server
if (rclcpp::spin_until_future_complete(node, result) ==
rclcpp::FutureReturnCode::SUCCESS)
{
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "%d", result.get()->success);
} else {
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Failed to call service add_two_ints");
}

// Shutdown the ROS2 communication
rclcpp::shutdown();

return 0;
}

CMakeLists.txt Update,

add_executable(client src/service_client.cpp) #Change
ament_target_dependencies(client rclcpp geometry_msgs custom_srv) #Change

install(TARGETS
client
server #Change
DESTINATION lib/${PROJECT_NAME}
)

Now Compile and Run,

cd ~/ros2_ws/
colcon build --packages-select turtlebot_srv #Make sure you compiled custom_srv
source install/setup.zsh

# Terminal 1
ros2 launch turtlebot3_gazebo turtlebot3_world.launch.py # Launch Turtlebot3

# Terminal 2
ros2 launch turtlebot_srv service_server_client_launch.py # Launch the server

# Terminal 3
ros2 run turtlebot_srv client start # Launch the client with arg [start, stop]

In this hands-on guide, we walked through the process of creating custom messages (.msg) and service files (.srv) for use with the Turtlebot3 robot. We also demonstrated how to create a service node in ROS to handle requests and responses using these custom message and service types.

If you followed along with the tutorial, you should now have a solid foundation for creating custom messages and service nodes.

Thank you for reading, and I hope you found this guide helpful! If you have any questions or comments, feel free to leave them in the comments section below.

--

--

Jegathesan Shanmugam

Talks about #robotics, #computervision, and #embeddedsystems