Creating nodes
You can run this tutorial on:
Workspace setupβ
To begin developing your own nodes, you need to do some workspace configuration first. Workspace is the 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. You can do this by typing in:
mkdir -p ~/ros_workspace/src
This will create folder named ros_workspace
and folder src
inside it.
All of the source files for your nodes will be stored in folder src
.
Then you can initialize your workspace with command
catkin_init_workspace
executed in src
folder:
cd ~/ros_workspace/src
catkin_init_workspace
Now you can move to your workspace main directory:
cd ~/ros_workspace
and compile it:
catkin_make
After this command you should get output like this:
And it should end with:
####
#### Running command: "make -j4 -l4" in "/home/pi/ros_workspace/build"
####
After this operation you should have two new folders in your workspace:
build
for storing files that are used during compilation and devel
for storing output files.
Now your workspace is set up and ready for creating new nodes.
Creating new packageβ
As you should already know, in ROS, nodes are distributed in packages, so
in order to create a node you need to create a package. Packages are
created with command catkin_create_pkg
and it must be executed in
src
folder in your workspace.
Syntax of catkin_create_pkg
is:
catkin_create_pkg package_name [required packages]
where package_name
is desired package name and argument
required packages
is optional and contain names of packages that are used
by newly created packages.
For our tutorial we will create package named tutorial_pkg
which
depends on package roscpp
. Package roscpp
is a basic ROS library for
C++.
cd ~/ros_workspace/src
catkin_create_pkg tutorial_pkg roscpp
After typing in this command you should get output like this:
This will create folder named tutorial_pkg
and some files in it. Your
workspace file structure should now look like like below:
Created files are:
CMakeLists.txt
- these are build instructions for your nodes, you need to edit this file if you want to compile any node, we will do it later.package.xml
- this file contains package metadata like author, description, version or required packages. Package can be built without changing it, but you should adjust this file if you want to publish your package to others.
Write code for your first nodeβ
Letβs create C++ file for your node, name it tutorial_pkg_node.cpp
and
place it in src
folder under tutorial_pkg
:
touch ~/ros_workspace/src/tutorial_pkg/src/tutorial_pkg_node.cpp
Open file in your favourite text editor and paste:
#include <ros/ros.h>
int main(int argc, char **argv)
{
ros::init(argc, argv, "example_node");
ros::NodeHandle n("~");
ros::Rate loop_rate(50);
while (ros::ok())
{
ros::spinOnce();
loop_rate.sleep();
}
}
Code explanation line by line:
#include <ros/ros.h>
Add header files for basic ROS libraries.
int main(int argc, char **argv) {
Beginning of node main function.
ros::init(argc, argv, "example_node");
Initialization of ROS node, this function contacts with ROS master and registers node in the system.
ros::NodeHandle n("~");
Get the handle for node, this handle is required for interactions with system e.g. subscribing to topic.
ros::Rate loop_rate(50);
Define rate for repeatable operations.
while (ros::ok()) {
Check if ROS is working. E.g. if ROS master is stopped or there was sent
signal to stop the system, ros::ok()
will return false.
ros::spinOnce();
Process all incoming messages.
loop_rate.sleep();
Wait until defined time passes.
You can save the C++ file.
Building your nodeβ
Before you build the node, you need to edit CMakeLists.txt
from
tutorial_pkg
directory. Open it in your favourite text editor.
Find line:
# add_compile_options(-std=c++11)
and uncomment it (remove #
sign). This will allow to use C++11 standard of C++.
You should also find and uncomment line:
# add_executable(${PROJECT_NAME}_node src/tutorial_pkg_node.cpp)
This will let the compiler know that it should create executable
file from defined source. Created executable
will be your node. Variable PROJECT_NAME
is defined by line project(tutorial_pkg)
.
This results in tutorial_pkg_node
as the name of the executable. You can adjust it to your needs.
After that find and uncomment lines:
# target_link_libraries(${PROJECT_NAME}_node
# ${catkin_LIBRARIES}
# )
This will cause compiler to link libraries required by your node. Save the changes and close editor.
Final CMakeLists.txt
should look like this:
cmake_minimum_required(VERSION 2.8.3)
project(tutorial_pkg)
add_compile_options(-std=c++11)
find_package(catkin REQUIRED COMPONENTS
roscpp
)
catkin_package(
CATKIN_DEPENDS
)
include_directories(${catkin_INCLUDE_DIRS})
add_executable(${PROJECT_NAME}_node src/tutorial_pkg_node.cpp)
target_link_libraries(${PROJECT_NAME}_node
${catkin_LIBRARIES}
)
Open terminal, move to workspace main directory and build your project
with command catkin_make
:
cd ~/ros_workspace
catkin_make
You should get output like this:
Running your nodeβ
Your node is built and ready for running, but before you run it, you need to load some environment variables:
source ~/ros_workspace/devel/setup.sh
These environment variables allow you to run node regardless of directory you are working in. You have to load it every time you open new terminal or you can add line:
. ~/ros_workspace/devel/setup.sh
to your .bashrc
file.
To run your node you can use command line or .launch
file as with any
other node. Remember that package is tutorial_pkg
and node is
tutorial_pkg_node
.
Task 1 Run your node with command line or .launch
file. Then use
rosnode
and rqt_graph
tools to examine system and check if your node
is visible in the system.
To remind, you can start ROS by typing in the name of the node, you can do this with the following command:
rosrun package_name node_type [options]
For the node you just created it will be:
rosrun tutorial_pkg tutorial_pkg_node
If you want to use .launch
files associated with your custom package you will have to create launch
directory:
mkdir ~/ros_workspace/src/tutorial_pkg/launch
Place your .launch
files there. This way you can start them by typing:
roslaunch tutorial_pkg your_launch_file.launch
Example launch file for tutorial_pkg_node
will be as follows:
<launch>
<node pkg="tutorial_pkg" type="tutorial_pkg_node" name="tutorial_pkg_node" output="screen">
</node>
</launch>
Save it as tutorial_pkg_node.launch
in ~/ros_workspace/src/tutorial_pkg/launch
directory and launch it:
roslaunch tutorial_pkg tutorial_pkg_node.launch
Subscribing to topicβ
You will modify your node to subscribe to topic
/camera/rgb/image_raw
and calculate average brightness of image.
To process message received from the camera you need a header file with message type definition. You can include it with:
#include <sensor_msgs/Image.h>
Image message is an object consisting of following fields:
std_msgs/Header header
- header with message metedatauint32 height
- image height in pixelsuint32 width
- image width in pixelsstring encoding
- pixel encoding definitionuint8 is_bigendian
- is data expressed in bigendian manneruint32 step
- length of data for one rowstd::vector<uint8_t> data
- actual image data
Then you need a function for processing received message:
void imageCallback(const sensor_msgs::ImageConstPtr &image)
{
long long sum = 0;
for (int value : image->data)
{
sum += value;
}
int avg = sum / image->data.size();
std::cout << "Brightness: " << avg << std::endl;
}
Code explanation line by line:
void imageCallback(const sensor_msgs::ImageConstPtr &image)
Function definition, argument is pointer to incoming message.
long long sum = 0;
Variable for storing sum of all pixel values.
for( int value : image->data )
Iteration through every pixel and colour.
sum+=value;
Add current pixel value to sum.
int avg = sum/image->data.size();
Calculate average value.
std::cout << "Brightness: " << avg << std::endl;
Print brightness value to screen.
Last thing to do is defining topic to subscribe:
ros::Subscriber sub = n.subscribe("/camera/rgb/image_raw", 10, imageCallback);
Here we use method subscribe
of NodeHandle
object. Arguments of
method are:
/camera/rgb/image_raw
- name of topic to subscribe.10
- message queue size. Messages are processed in order they come in. In the case that node receives, in short time, more messages than this value, excessive messages will be dropped.imageCallback
- function to process incoming messages.
Your final code should look like this:
#include <ros/ros.h>
#include <sensor_msgs/Image.h>
void imageCallback(const sensor_msgs::ImageConstPtr &image)
{
long long sum = 0;
for (int value : image->data)
{
sum += value;
}
int avg = sum / image->data.size();
std::cout << "Brightness: " << avg << std::endl;
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "example_node");
ros::NodeHandle n("~");
ros::Subscriber sub = n.subscribe("/camera/rgb/image_raw", 10, imageCallback);
ros::Rate loop_rate(50);
while (ros::ok())
{
ros::spinOnce();
loop_rate.sleep();
}
}
Task 2 Build your node and run it along with astra.launch
. Use
rosnode
, rostopic
and rqt_graph
tools to examine system and check
how data is passed between nodes.
When using launch files we can make use of prevoiously created files, this way we can make configuration easier and more readable.
Instead of configuring tutorial_pkg_node
again, we will include file created in prevoius step:
<launch>
<arg name="use_gazebo" default="false"/>
<include unless="$(arg use_gazebo)" file="$(find astra_launch)/launch/astra.launch"/>
<include if="$(arg use_gazebo)" file="$(find rosbot_description)/launch/rosbot.launch"/>
<include file="$(find tutorial_pkg)/launch/tutorial_pkg_node.launch"/>
</launch>
Save above file as tutorial_2.launch
in ~/ros_workspace/src/tutorial_pkg/launch
directory and launch it:
roslaunch tutorial_pkg tutorial_2.launch
or if you are using Gazebo simulator:
roslaunch tutorial_pkg tutorial_2.launch use_gazebo:=true
Receiving parametersβ
Your node can receive parameters, they are used to customize behaviour of node e.g. subscribed topic name, device name or transmission speed for serial port.
You will modify a node to receive boolean parameter which defines if node should print image brightness to screen.
To receive the parameter you need a variable to store its value, in this example variable should have a global scope:
bool print_b;
Then receive parameter value:
n.param<bool>("print_brightness", print_b, false);
Here we use method param
of NodeHandle
object. Arguments of method
are:
print_brightness
- name of parameter to receive.print_b
- variable to store parameter value.false
- parameter default value.
Last thing is to print brightness dependant on parameter value:
if (print_b)
{
std::cout << "Brightness: " << avg << std::endl;
}
Your final code should look like this:
#include <ros/ros.h>
#include <sensor_msgs/Image.h>
bool print_b;
void imageCallback(const sensor_msgs::ImageConstPtr &image)
{
long long sum = 0;
for (int value : image->data)
{
sum += value;
}
int avg = sum / image->data.size();
if (print_b)
{
std::cout << "Brightness: " << avg << std::endl;
}
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "example_node");
ros::NodeHandle n("~");
ros::Subscriber sub = n.subscribe("/camera/rgb/image_raw", 10, imageCallback);
n.param<bool>("print_brightness", print_b, false);
ros::Rate loop_rate(50);
while (ros::ok())
{
ros::spinOnce();
loop_rate.sleep();
}
}
Task 3 Run your node with parameter print_brightness
set to true
and again set to false
. Observe how behaviour of node changes.
To add parameter for node, you will need to add <param>
tag inside <node>
tag in tutorial_pkg_node.launch
file.
Publishing to topicβ
You will modify node to publish brightness value to a new topic with
message of type std_msgs/UInt8
. Message std_msgs/UInt8
is object
with only one field data
, which contain actual integer data.
Begin with including message header file:
#include <std_msgs/UInt8.h>
Next define publisher object with global scope:
ros::Publisher brightness_pub;
Then register in the system to publish to a specific topic:
brightness_pub = n.advertise<std_msgs::UInt8>("brightness" , 1);
Here we use method advertise
of NodeHandle
object. Arguments of
method are:
brightness
- topic name.1
- message queue size.
You also need to declare type of message which will be published, in this
case it is std_msgs::UInt8
.
Last thing is to put some data into message and send it to topic with some frequency:
std_msgs::UInt8 brightness_value;
brightness_value.data=avg;
brightness_pub.publish(brightness_value);
In our example it can be done while processing each message incoming from camera topic.
Your final code should look like this:
#include <ros/ros.h>
#include <sensor_msgs/Image.h>
#include <std_msgs/UInt8.h>
bool print_b;
ros::Publisher brightness_pub;
void imageCallback(const sensor_msgs::ImageConstPtr &image)
{
long long sum = 0;
for (int value : image->data)
{
sum += value;
}
int avg = sum / image->data.size();
if (print_b)
{
std::cout << "Brightness: " << avg << std::endl;
}
std_msgs::UInt8 brightness_value;
brightness_value.data = avg;
brightness_pub.publish(brightness_value);
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "example_node");
ros::NodeHandle n("~");
ros::Subscriber sub = n.subscribe("/camera/rgb/image_raw", 10, imageCallback);
n.param<bool>("print_brightness", print_b, false);
brightness_pub = n.advertise<std_msgs::UInt8>("brightness", 1);
ros::Rate loop_rate(50);
while (ros::ok())
{
ros::spinOnce();
loop_rate.sleep();
}
}
Task 4 Compile your node and run it along with astra.launch
. Use rosnode
,
rostopic
and rqt_graph
tools to examine the system, then use
rostopic echo
tool to read brightness of the image from the camera.
Calling the serviceβ
You will modify node to call to a service with message type
std_srvs/Empty
, this type has no field and can not carry any data, it
can be used only for invoking action in another node and getting reply
when its done.
As a service provider we will use image_saver
node from image_view
package. Image_saver
have one service named save
. every time it is
called, one frame from subscribed image topic is saved to hard drive.
Desired node behaviour is to count incoming frames and call service once per given number of frames.
Begin with importing required header files:
#include <std_srvs/Empty.h>
We need one variable for counting passed frames:
int frames_passed = 0;
In imageCallback
function increment counter with every incoming
message:
frames_passed++;
Create a client which will be caling to service:
ros::ServiceClient client = n.serviceClient<std_srvs::Empty>("/image_saver/save");
Here we use method serviceClient
of NodeHandle
object. Method has
only one argument, it is the name of service. You also need to determine
message type for service: std_srvs::Empty
.
Instantiate message object:
std_srvs::Empty srv;
Check if required number of frames passed and reset counter:
if (frames_passed > 100)
{
frames_passed = 0;
Call the service:
client.call(srv);
Your final code should look like this:
#include <ros/ros.h>
#include <sensor_msgs/Image.h>
#include <std_msgs/UInt8.h>
#include <std_srvs/Empty.h>
bool print_b;
ros::Publisher brightness_pub;
int frames_passed = 0;
void imageCallback(const sensor_msgs::ImageConstPtr &image)
{
long long sum = 0;
for (int value : image->data)
{
sum += value;
}
int avg = sum / image->data.size();
if (print_b)
{
std::cout << "Brightness: " << avg << std::endl;
}
std_msgs::UInt8 brightness_value;
brightness_value.data = avg;
brightness_pub.publish(brightness_value);
frames_passed++;
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "example_node");
ros::NodeHandle n("~");
ros::Subscriber sub = n.subscribe("/camera/rgb/image_raw", 10, imageCallback);
n.param<bool>("print_brightness", print_b, false);
brightness_pub = n.advertise<std_msgs::UInt8>("brightness", 1);
ros::ServiceClient client = n.serviceClient<std_srvs::Empty>("/image_saver/save");
std_srvs::Empty srv;
ros::Rate loop_rate(50);
while (ros::ok())
{
ros::spinOnce();
if (frames_passed > 100)
{
frames_passed = 0;
client.call(srv);
}
loop_rate.sleep();
}
}
Task 5 Build your node and run it with astra.launch
and image_saver
.
Use rosnode
, rostopic
and rqt_graph
tools to examine the system
and check how data is passed between nodes. Let the nodes work for a
certain time. Observe as new frames are being saved to your workspace
directory.
For image_saver
node you can create separate launch file:
<launch>
<node pkg="image_view" type="image_saver" name="image_saver">
<param name="save_all_image" value="false" />
<param name="filename_format" value="$(env HOME)/ros_workspace/image%04d.%s"/>
<remap from="/image" to="/camera/rgb/image_raw"/>
</node>
</launch>
Save it as image_saver.launch
in ~/ros_workspace/src/tutorial_pkg/launch
directory and include it in tutorial_2.launch
:
<launch>
<arg name="use_gazebo" default="false"/>
<include unless="$(arg use_gazebo)" file="$(find astra_launch)/launch/astra.launch"/>
<include if="$(arg use_gazebo)" file="$(find rosbot_description)/launch/rosbot.launch"/>
<include file="$(find tutorial_pkg)/launch/tutorial_pkg_node.launch"/>
<include file="$(find tutorial_pkg)/launch/image_saver.launch"/>
</launch>
To delete image files created by this example run following command in your ros_workspace
directory:
rm $(find image*)
Providing a serviceβ
You will modify node to provide a service, which returns information
regarding how many images were saved. This service will have a message
type std_srvs/Trigger
, it has no field for request and two fields for
response: integer to indicate if service was triggered successfully or
not and string for short summary of executed action.
Start with including required header files:
#include <std_srvs/Trigger.h>
Add variable for storing number of saved images:
int saved_imgs = 0;
Next, you need a function to execute when service is called:
bool saved_img(std_srvs::Trigger::Request &req, std_srvs::Trigger::Response &res)
{
res.success = 1;
std::string str("Saved images: ");
std::string num = std::to_string(saved_imgs);
str.append(num);
res.message = str;
return true;
}
Arguments for this function are pointers to request and response data. All services are called the same way, even if it does not carry any data, in that case these are pointer of void type.
Prepare string with response description:
std::string str("Saved images: ");
std::string num = std::to_string(saved_imgs);
str.append(num);
Fill string field with data:
res.message= str;
Fill integer field with data, this mean service was executed properly:
res.success=1;
Finish function, response will be sent to requesting node:
return true;
next thing to do is to increment image counter after saving frame:
saved_imgs++;
Last thing to do is to register provided service in the system:
ros::ServiceServer service = n.advertiseService("saved_images", saved_img);
Here we use method advertiseService
of NodeHandle
object. Arguments
of method are:
saved_images
- service name.saved_img
- method to execute.
Your final code should look like this:
#include <ros/ros.h>
#include <sensor_msgs/Image.h>
#include <std_msgs/UInt8.h>
#include <std_srvs/Empty.h>
#include <std_srvs/Trigger.h>
bool print_b;
ros::Publisher brightness_pub;
int frames_passed = 0;
int saved_imgs = 0;
void imageCallback(const sensor_msgs::ImageConstPtr &image)
{
long long sum = 0;
for (int value : image->data)
{
sum += value;
}
int avg = sum / image->data.size();
if (print_b)
{
std::cout << "Brightness: " << avg << std::endl;
}
std_msgs::UInt8 brightness_value;
brightness_value.data = avg;
brightness_pub.publish(brightness_value);
frames_passed++;
}
bool saved_img(std_srvs::Trigger::Request &req, std_srvs::Trigger::Response &res)
{
res.success = 1;
std::string str("Saved images: ");
std::string num = std::to_string(saved_imgs);
str.append(num);
res.message = str;
return true;
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "example_node");
ros::NodeHandle n("~");
ros::Subscriber sub = n.subscribe("/camera/rgb/image_raw", 10, imageCallback);
n.param<bool>("print_brightness", print_b, false);
brightness_pub = n.advertise<std_msgs::UInt8>("brightness", 1);
ros::ServiceClient client = n.serviceClient<std_srvs::Empty>("/image_saver/save");
std_srvs::Empty srv;
ros::ServiceServer service = n.advertiseService("saved_images", saved_img);
ros::Rate loop_rate(50);
while (ros::ok())
{
ros::spinOnce();
if (frames_passed > 100)
{
frames_passed = 0;
client.call(srv);
saved_imgs++;
}
loop_rate.sleep();
}
}
Task 6 Build your node and run it as in previous task. Use
rosnode
, rostopic
and rqt_graph
tools to examine the system.
Use rosservice call
tool to call service provided by your node. Usage
of rosservice
is analogical to rostopic
. To call service type:
rosservice call /tutorial_pkg_node/saved_images
As a response you should get something like this:
Message typesβ
In ROS there are many message types defined, they are grouped in packages accordingly to their application:
- messages with basic data types like integer or float
- for handling data from sensors
- for handling maps or robot localization data
- for handling information regarding object position, orientation, velocity or acceleration, also for defining points or polygons
While developing nodes for mobile robots you will probably want to use some of this messages:
- it has fields for determining position , velocity and effort for a set of joints.
- message for handling scans of planar laser scanners like RpLidar or Hokuyo.
- message for one dimensional distance measurement
- message for handling flat occupancy grid based maps.
- message for defining robot position and orientation based on odometry measurements.
- message for defining robot path as a set of positions
- message for defining linear and angular object velocity. Could be also used for setting desired velocities.
It is also possible to implement user defined messages if existing ones are not sufficient.
Summaryβ
After completing this tutorial you should be able to write code for your own node, build and run it in the same way as it can be done with nodes provided with ROS.
Your node may be configured with parameters and it can:
subscribe topics published by other nodes
publish new topic
provide service
call service provided by other nodes
by Εukasz Mitka, Husarion
Do you need any support with completing this tutorial or have any difficulties with software or hardware? Feel free to describe your thoughts on our community forum: https://community.husarion.com/ or to contact with our support: support@husarion.com