Conventions

ROS packages

ROS packages are defined in the ros2_ws/src directory.

Folder Structure

We try to follow this folder structure for the ROS packages:

ros_package/
|   CMakeLists.txt
|   package.xml
|
└───src/
|   |   cpp_node.cpp
|
└───include/
|   └───ros_package/
|       |   cpp_header.hpp
|
└───src_py/
|   |   py_node.py
|
└───ros_package/
|   |   __init__.py
|   |   py_module.py
│   └── test/
│       ├── conftest.py
│       ├── unit/
│       │   └── test_py_module.py
│       ├── integration/
│       │   └── test_service_client.py
│       └── end_to_end/
│           └── test_full_launch.py
└───launch/
    |   launch_file.launch.py
  • Every ROS package contains the CMakeLists.txt and package.xml in the root directory.

  • In case of development using C++, the executables are located in the src/ directory. The corresponding headers are located in the include/ directory, inside a sub-directory that equals the package name.

  • In case of development using Python, the executables are located in the src_py/ directory.

  • A sub-directory ros_package/ with the same name as the ROS package can be used to create a Python package. This directory contains an __init__.py file and the Python modules of the Python package.

  • Possible launch files are located in the launch/ directory.

  • More directories are possible, like urdf/ for urdf files or config/ for config files.

  • The testing layout is placed alongside the Python package, with these subfolders:

    • unit/ for fast, logic-only tests

    • integration/ for multi-component or ROS client/server tests

    • end_to_end/ for full launch/simulation tests

    Use a top-level test/conftest.py to define shared fixtures, and name your files/functions test_* so pytest auto-discovers them. The most general fixtures are defined in the conftest.py in the root of the repository.

CMakeLists.txt

This CMakeLists.txt file shows the different parts required to build a ROS package:

 1# SPDX-FileCopyrightText: Alliander N. V.
 2#
 3# SPDX-License-Identifier: Apache-2.0
 4
 5cmake_minimum_required(VERSION 3.5)
 6project(ros_package)
 7
 8# CMake dependencies:
 9find_package(ament_cmake REQUIRED)
10find_package(ament_cmake_python REQUIRED)
11
12# Other dependencies:
13find_package(geometry_msgs REQUIRED)
14find_package(vision_msgs REQUIRED)
15
16# C++ executables:
17add_executable(cpp_node src/cpp_node.cpp)
18ament_target_dependencies(cpp_node geometry_msgs vision_msgs)
19install(
20  TARGETS cpp_node
21  DESTINATION lib/${PROJECT_NAME}
22)
23
24# Python executables:
25install(
26  DIRECTORY src_py/
27  DESTINATION lib/${PROJECT_NAME}
28)
29
30# Python package:
31ament_python_install_package(${PROJECT_NAME})
32
33# Shared folders:
34install(
35  DIRECTORY launch
36  DESTINATION share/${PROJECT_NAME}
37)
38
39# Default test:
40if(BUILD_TESTING)
41  find_package(ament_lint_auto REQUIRED)
42  ament_lint_auto_find_test_dependencies()
43endif()
44
45ament_package()

5-10:
The file always starts with a version definition, the package name and the CMake dependencies when building C++ and/or python files.

12-14:
If the package depends on other packages, these are defined. In this case, the packaged depends on the vision_msgs and geometry_msgs packages.

16-22:
Building a C++ executable requires 3 steps: defining the executable, linking dependencies (if any) and installing the targets to the lib directory.

24-28:
For Python executables, we can simply install them all at the same time, by providing the directory.

30-31:
If the package contains a Python package, it needs to be installed.

33-37:
All shared folders are installed into the share directory. This includes the directory of launch files, but also other possible directories, like urdf/ or config/, if these exist.

39-45:
The file always ends with a default test and the ament_package() command.

package.xml

The package.xml file is related to the CMakeLists.txt file:

 1<?xml version="1.0"?>
 2<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
 3
 4<!--
 5SPDX-FileCopyrightText: Alliander N. V.
 6
 7SPDX-License-Identifier: Apache-2.0
 8-->
 9
10<package format="3">
11  <name>ros_package</name>
12  <version>0.1.0</version>
13  <description>A ros package.</description>
14  <maintainer email="researchcenter@alliander.com">RCDT</maintainer>
15  <license>Apache 2.0</license>
16
17  <buildtool_depend>ament_cmake</buildtool_depend>
18  <buildtool_depend>ament_cmake_python</buildtool_depend>
19
20  <depend>geometry_msgs</depend>
21  <depend>vision_msgs</depend>
22
23  <test_depend>ament_lint_auto</test_depend>
24
25  <export>
26    <build_type>ament_cmake</build_type>
27  </export>
28</package>

1-2:
The files starts with default xml definitions.

10-15:
Inside the package tag, we start with some general information about the package.

17-18:
Next, we define the build tool dependencies for building C++ and/or Python files.

20-21:
Next, we define other packages where our package depends on.

23-28:
The file ends with the default test dependency and an export definition.

Custom Messages, Services and Actions

We define custom messages, services and actions in our rcdt_interfaces package. This package has the following folder structure:

ros_package/
|   CMakeLists.txt
|   package.xml
|
└───msg/
|   |   custom_message_definition.msg
|
└───srv/
|   |   custom_service_definition.srv
|
└───action/
    |   custom_action_definition.action

In the CMake file, we automatically look for all msg, srv and action files and generate interfaces for them:

 1# SPDX-FileCopyrightText: Alliander N. V.
 2#
 3# SPDX-License-Identifier: Apache-2.0
 4
 5cmake_minimum_required(VERSION 3.5)
 6project(ros_package)
 7
 8# CMake dependencies:
 9find_package(ament_cmake REQUIRED)
10find_package(rosidl_default_generators REQUIRED)
11
12# Other dependencies:
13find_package(geometry_msgs REQUIRED)
14find_package(vision_msgs REQUIRED)
15
16file(GLOB MSGS CONFIGURE_DEPENDS msg/*.msg*)
17file(GLOB SRVS CONFIGURE_DEPENDS srv/*.srv*)
18file(GLOB ACTIONS CONFIGURE_DEPENDS action/*.action*)
19
20foreach(file IN LISTS MSGS)
21  string(REPLACE "${CMAKE_CURRENT_SOURCE_DIR}/" "" file_relative ${file})
22  list(APPEND MSGS_STRIPPED ${file_relative})
23endforeach()
24
25foreach(file IN LISTS SRVS)
26  string(REPLACE "${CMAKE_CURRENT_SOURCE_DIR}/" "" file_relative ${file})
27  list(APPEND SRVS_STRIPPED ${file_relative})
28endforeach()
29
30foreach(file IN LISTS ACTIONS)
31  string(REPLACE "${CMAKE_CURRENT_SOURCE_DIR}/" "" file_relative ${file})
32  list(APPEND ACTIONS_STRIPPED ${file_relative})
33endforeach()
34
35rosidl_generate_interfaces(${PROJECT_NAME}
36  ${MSGS_STRIPPED}
37  ${SRVS_STRIPPED}
38  ${ACTIONS_STRIPPED}
39  DEPENDENCIES geometry_msgs vision_msgs
40)
41
42# Default test:
43if(BUILD_TESTING)
44  find_package(ament_lint_auto REQUIRED)
45  ament_lint_auto_find_test_dependencies()
46endif()
47
48ament_package()

To generate custom messages successfully, we also need to specify the following dependencies in the package.xml file:

  <buildtool_depend>rosidl_default_generators</buildtool_depend>
  <member_of_group>rosidl_interface_packages</member_of_group>

The Service Structure

Unless more data is required, the Response of our services contains the following data by default:

---
bool success
string message

When our service only requires a single request datatype, and the default response as described above, the name of the service will simply be the datatype of the request + Srv. So for example, when the only datatype in the request is a string, the service will be called StringSrv.srv.

In case the service is more complex, the name of the service will represent its purpose. E.g., if the intention of the service is to add an object to a scene, the name of the service will be AddObject.srv.

The Action Structure

Unless more data is required, the Response and Feedback of our actions contains the following data by default:

---
bool success
string message
---
string status

When our action only requires a single request datatype, and the default response & feedback as described above, the name of the action will simply be the datatype of the request + Action. So for example, when the only datatype in the request is a string, the action will be called StringAction.action.

In case the action is more complex, the name of the action will represent its purpose. E.g., if the intention of the action is to move an object to a location in the scene, the name of the action will be MoveObjectToLocation.action.