Skip to content
Kosmas Tsiakas edited this page Dec 15, 2022 · 37 revisions

ROS Best Practices

This is a loose collection of best practices, conventions, and tricks for using the Robot Operating System (ROS). It builds up on the official ROS documentation and other resources and is meant as summary and overview.

Official ROS documentation:

Other References:

In parts, the document describes opinionated best practices established within the Robotic Systems Lab and Autonomous Systems Lab, ETH Zurich and ANYbotics.

Author: Péter Fankhauser, pfankhauser@anybotics.com
Affiliation: ANYbotics

This work is conducted as part of ANYmal Research, a community to advance legged robotics.

TODO

Before You Begin

Research other products out there already: http://www.ros.org/browse/list.php

Coding Guidelines

Refer to the ROS C++ Style Guide. At the Robotic Systems Lab and ANYbotics we use an adapted version of the Google style-guide.

Units and Coordinate Conventions

Refer to Standard Units of Measure and Coordinate Conventions.

Coordinate Frames

Testing

Refer to http://wiki.ros.org/UnitTesting.

Package Organization

  • The overhead of a ROS package is not large. Define separate packages wherever they make sense. Often, code can be useful in contexts other than those for which it was built.
  • Avoid combining nodes that pull in mutually unneeded dependencies and are often used separately (to eliminate unnecessary build overhead).
  • The package dependency graph must be acyclic, i.e. no package may depend on another that directly or indirectly depends on it.
  • If programs with similar dependencies are generally used together, consider combining them into a single package.
  • If some nodes have common dependencies on shared code that you do not wish to export publicly, they can be combined internally within a single package.
  • Create separate packages that contain only messages, services and actions (separation of interface and implementation). Examples for separate message packages are the ros/common_msgs packages.
  • Group packages in stacks.

Sources:

References:

Package Naming

Refer to Naming ROS Resources and REP-144: ROS Package Naming (draft).

Choose the name carefully:

  • They are messy to change later.
  • Package names are global to the entire ROS ecosystem.
  • Try to pick names that will make sense to others who may wish to use your code.
  • Package names should be specific enough to identify what the package does. Do not over scope, e.g. planner is a bad name, use wavefront_planner instead.
  • Do not use “utils” or other catchalls.
  • Prefixing a package name is recommended only when the package is not meant to be used more widely (e.g., packages that are specific to the StlarETH robot use the ‘starleth_’ prefix).

Naming Conventions for Packages, Nodes, Topics, Services, TF etc.

Adapted from ROS Best Practices: Lorenz Mösenlechner, Technische Universität München, July 2012:

  • Package names are lower case.
  • Packages must not contain dashes (“-”), only underscores (“_”).
  • Nodes, topics, services, actions, parameters are all lower case with underscores as separator.
  • Messages, services and actions are named in camel case: geometry_msgs/PoseStamped.
  • Names in a message/service/action definition are all lower case with underscores as separator: geometry_msgs/Pose end_effector.
  • Do not use the word “action” in an action definition: Foo.action, not FooAction.action.

Custom ROS Message and Services

  • Use standard data types whenever possible (try to prevent .msg proliferation)! For example, instead of creating a custom EstimatorUpdateTime.msg, use the std_msgs/Time.msg definition. Another example is an empty service call TriggerComputation.msg, use [std_srvs/Empty.srv] (http://docs.ros.org/api/std_srvs/html/srv/Empty.html) instead.

  • Do not define a new msg/srv/action definition for each topic/service/action! For example, instead of creating two definitions LoadMapFromFile.srv and SaveMapToFile.srv with the same content

      string file_path
      —
    

    define one type ‘ProcessFile.srv’ which can be used from both services, ~/load_map and ~/save_map, respectively.

  • Complex messages are built through composition (e.g. geometry_msgs/PoseWithCovarianceStamped).

  • Try to avoid building messages that tend to not get completely filled out.

References:

Documentation

  • Create a short README.md/Wiki for each package:
    • Document what the node does,
    • Document topics, services and actions that are required and provided,
    • Document ROS parameters and their default values, A template for the README.md is provided here
  • Provide launch files,
  • Provide a rosinstall file.

File/Folder Structure for Packages

Use this file/folder structure for a general ROS package:

package_name
|— config
	|— robots
		|— my_robot.yaml
	|— sensors
		|— velodyne.yaml
		|— hokuyo_laser_range.yaml
|— include/package_name
	|— Class1.hpp
	|— Class2.hpp
|— launch
	|— node1_name.launch
	|— node2_name.launch
|— rviz
	|— package_name.rviz
|— scripts
	|— my_script.py
|— src
	|— Class1.cpp
	|— Class2.cpp
	|— node1_name_node.cpp
	|— node2_name_node.cpp
|— test
	|— Class1Test.cpp
	|— Class2Test.cpp
	|— test_package_name.cpp
|— CMakeLists.txt
|— package.xml

For ROS message and service definitions use:

package_name_msgs
|— action
	|— MyAction.action
|— msg
	|— MyMessage.msg
|— srv
	|— MyService.srv
|— CMakeLists.txt
|— package.xml

References:

Topics vs Services vs Actionlib vs Parameters vs Dynamic Parameters

Refer to ROS Patterns - Communication.

Summary:

  • Use topics for publishing continuous streams of data, e.g. sensor data, continuous detection results, …
  • Use services only for short calculations.
  • Use actions for all longer running processes, e.g. grasping, 
navigation, perception, …
  • Use parameters for values which are known at launch and are not likely to change during run time.
  • Use dynamic parameters (dynamic_reconfigure) for parameter which are likely to change during run time.

Publishing Spatial / Geometric Data

Refer to http://wiki.ros.org/ROS/Patterns/Communication.

Node Handles

There are four main types of node handles:

  1. Default (public) node handle: nh_ = ros::NodeHandle();
  2. Private node handle: nh_private_ = ros::NodeHandle(“~”);
  3. Namespaced node handle: nh_aslam_ = ros::NodeHandle(“aslam”);
  4. Global node handle: nh_global_ = ros::NodeHandle(“/“); (You probably shouldn’t use this ever.)

Generally you will only use the first 2 -- you could also use the namespaced node handle for separating out publishers for nodes that have many.

To explain what these do and how they should be used, let’s assume your ROS node is named ros_node, in namespace blah, and you are trying to look up the name topic. Here is what they will resolve to using all 4 node handles:

  1. /blah/topic
  2. /blah/ros_node/topic
  3. /blah/aslam/topic
  4. /topic

If, instead, your try to resolve /topic, this will skip the namespace of the node and resolve to /topic.

When to Use Which Node Handle

These are just general guidelines, but when possible, prefer to use the following in each case:

  • Subscribers - usually public node handles.
  • Publishers - usually private node handles for most output/visualization, occasionally necessary to use public for globally-used data (i.e., /odom topic).
  • Parameters - almost always private node handle.

Never use global names. This is because they do not resolve properly when you push nodes into namespaces, and does not allow you to run more than one of your node at a time properly. Or use multiple robots on the same master. Define published topics and parameters relative to the nodes namespace:

Good: odometry, grid_map, cam0/camera_info
Bad: /odometry, /grid_map, /helicopter/cam0/camera_info

Topic Naming

Topics should be named in the context of the node. Very simple and clear names are preferred for a easy to understand “ROS API”. Topic names not cause collision as long as they are published within the namespace of the node (see Namespace for Topics and Parameters).

In order to tell another node where to subscribe, set the topic name as ROS parameter (preferred). Alternatively, for third-party nodes, you can use the remap tag in roslaunch.

References:

Parameter Naming

Use a hierarchical scheme for parameters, such as

camera/left/name: left_camera
camera/left/exposure: 1
camera/right/name: right_camera
camera/right/exposure: 1.1

instead of

camera_left_name: left_camera

etc. This protects parameter names from colliding and allows parameters to be access individually or as a tree. In a YAML-file, the structure would be

camera:
  left:
    name: left_camera
    exposure: 1
  right:
    name: right_camera
    exposure: 1.1

References:

Parameter Organisation

If your node has only one or two parameters, you can set them in a launch file with the <param> tag:

<launch>
	<node pkg="my_package" type="my_node" name="my_name" output="screen">
		<param name="my_parameter" value="10" />
	</node>
</launch>

In general (preferred), organize the parameters in YAML-files and load them via the rosparam-tag:

<launch>
	<node pkg="my_package" type="my_node" name="my_name" output="screen">
		<rosparam command="load" file="$(find my_package)/config/robots/starleth.yaml" />
		<rosparam command="load" file="$(find my_package)/config/sensors/default.yaml" />
	</node>
</launch>

Moreover, you can decide to always load a set of default parameters, again using the rosparam-tag, and then allow the user of the launch file to overlay/overwrite these default settings. An example is provided by ros_package_template_overlying_params.launch. The user of this launch file can then do something like

<launch>
	<include file="$(find ros_package_template)/launch/ros_package_template_overlying_params.launch">
		<arg name="overlying_param_file"  value="$(find my_package)/config/robots/custom_params.yaml" />
	</include>
</launch>

Do not use command line parameters but the ROS parameter server. For parameters that are likely to change at runtime, use dynamic_reconfigure.

References:

Using Third-Party Libraries

Encourages standalone libraries with no ROS dependencies. Don’t put ROS dependencies in the core of your algorithm!

If you can develop a ROS independent library and release a parallel ROS wrapper

https://web.archive.org/web/20200331162114/courses.csail.mit.edu/6.141/spring2012/pub/lectures/Lec06-ROS.pdf

Refer to Using Third-Party Libraries.

  • If possible, try to use libraries from Debian packages.
  • Specify rosdep dependencies (tool for installing system packages).
  • If you need to compile a library from source create a ROS wrapper package that downloads and compiles the package.
  • Don’t use sudo in wrapper packages.
  • Don’t require manual system wide installations.
  • Don’t copy libraries into packages that need them.

Building

Never call cmake by hand in a package.

Dependencies

Keep your dependencies clean:

  • Only depend on what you need,
  • Specify all dependencies,
  • Do not use implicit dependencies.

If multiple runs of catkin_make are required for your workspace to be built, something is fishy!

Startup Order

Do not require a specific startup order for nodes. Use waitForService, waitForTransform, waitForServer, …

Roslaunch Organization

Refer to Roslaunch tips for large projects.

<include file=“$(find package_name)/launch/another.launch”/>

Printing Messages/Logging

  • Use rosconsole utilities for logging(ROS_INFO,ROS_DEBUG, …).
  • Use appropriate console logging: Debug, info, warn, error, fatal.
  • Provide introspection/debug topics.

Debugging

Debugging through VS Code ROS debugger. Install ROS extension

In a VS Code workspace there is a directory named .vscode. You need to create a file launch.json there, with the following content:

{
"configurations": [
       {
           "name": "ROS: Attach",
           "type": "ros",
           "request": "attach"
       },
       {
           "name": "ROS: Attach to Python",
           "type": "ros",
           "request": "attach",
           "runtime": "Python"
       }
   ] 
}

Build the workspace catkin build --cmake-args -DCMAKE_BUILD_TYPE=Debug

You can launch the ROS nodes from wherever in your PC, either using rosrun or roslaunch.

Once the ROS nodes are running, navigate to the debug tab on the VS code. Click on the ROS: Attach button, choose if you need C++ or Python node to debug and type the ROS node name in order to select it.
You will be asked to enter your password.

Checking the Number of Subscribers

To avoid computational overhead for topics which no nodes are subscribed to, check the number of subscribers with

if (publisher.getNumSubscribers() < 1) return;

ROS Bag Files

  • Recording of a bag:

      rosbag record <topic> <topic> … 
    
  • Play a bag:

      rosbag play foo.bag 
    
  • Play a bag using recorded time (important when stamped data and TF was recorded):

      rosbag play --clock foo.bag
    

    Note: The /use_sim_time parameter must be set to true before the node is initialized.

      rosparam set use_sim_time true
    

References:

Time

Use ros::Time, ros::Duration, and ros::Rate instead of 
system time.

Converting Between ROS Messages and Other Types

Eigen

To convert to/from messages, use eigen_conversions (or kindr- or minkindr-conversions).

Example:

Eigen::Vector3d my_super_cool_vector(1.0, 2.0, 3.0);
geometry_msgs::Point point_msg;
tf::pointEigenToMsg(my_super_cool_vector, point_msg);
super_cool_publisher_.publish(point_msg);

To go to/from TF, use tf_conversions (or also kindr- or minkindr-conversions).

Example:

Eigen::Vector3d my_super_cool_vector(1.0, 2.0, 3.0);
tf::Vector3 my_super_cool_vector_tf;
tf::vectorEigenToTF(my_super_cool_vector, my_super_cool_vector_tf);
tf::Transform transform;
transform.setOrigin(my_super_cool_vector_tf);
transform_broadcaster_.sendTransform(
	tf::StampedTransform(transform, ros::Time::now(), “map”, “world”));

References:

OpenCV Image

Use the cv_bridge. This allows very easy conversions to/from ROS messages.

Example:

const stereo_msgs::DisparityImageConstPtr& msg;  // We got this from a subscription callback.
cv::Mat output_image;
cv_bridge::CvImageConstPtr cv_img_ptr = cv_bridge::toCvShare(msg->image, msg);
// This is a shallow copy.
output_image = cv_img_ptr->image;

cv_bridge::CvImage image_cv_bridge;
image_cv_bridge.header.frame_id = “map”;
image_cv_bridge.image = output_image;
publisher_.publish(image_cv_bridge.toImageMsg());

References:

Catkin Build Flags

These are some useful CMake flags for catkin. To use them with catkin_tools, add them as arguments with

catkin config [list of your flags]

So for example

catkin config -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER_ARG1=-std=c++11

Useful catkin build flags:

  • Build in C++ release mode

      -DCMAKE_BUILD_TYPE=Release
    
  • Build with C++11

      -DCMAKE_CXX_COMPILER_ARG1=-std=c++11
    
  • Build Eclipse projects

      -G"Eclipse CDT4 - Unix Makefiles"
    
  • Build Eclipse projects with C++11 indexing

      -G"Eclipse CDT4 - Unix Makefiles" -D__GXX_EXPERIMENTAL_CXX0X__=1 -D__cplusplus=201103L