Workspaces and Packages

First, make sure that the ROS setup is loaded. For instance for ROS2 humble

source /opt/ros/humble/setup.sh

or in general choosing the appropriate ROS distribution. This step can be performed automatically inserting the command inside the ~/.bashrc file.

Workspaces

Ros 2 exploits has a workspace structure as follows:

/path/to/workspace
├── build
│   ├── COLCON_IGNORE
│   ├── ros-influxdb-bridge
│   └── ur_coppeliasim
├── install
│   ├── COLCON_IGNORE
│   ├── local_setup.bash
│   ├── local_setup.ps1
│   ├── local_setup.sh
│   ├── _local_setup_util_ps1.py
│   ├── _local_setup_util_sh.py
│   ├── local_setup.zsh
│   ├── ros-influxdb-bridge
│   ├── setup.bash
│   ├── setup.ps1
│   ├── setup.sh
│   ├── setup.zsh
│   └── ur_coppeliasim
├── log
│   ├── build_2023-11-07_14-41-48
│   ├── build_2023-11-07_15-35-32
│   ├── COLCON_IGNORE
│   ├── latest -> latest_build
│   └── latest_build -> build_2023-11-07_15-35-32
└── src
    ├── ros-influxdb-bridge
    └── ur_coppeliasim

Development happens only inside the source src folder. Everything else is generated by by the build system. In ros 2 this is accomplished by colcon with the command

colcon build

that must be called inside the root directory of the workspace. Then to let ROS see the available pacakge the setup script inside the workspace must be called

source /path/to/workspace/install/setup.sh

Even this command could be inserted in the ~/.bashrc file for convenience.

Package creation

Packages can be created with different programming languages, mainly python and C++.

Python package creation

Once inside the src folder inside a workspace, a new python package (e.g. my_package) can be created with the command

ros2 pkg create --build-type ament_python my_package --dependencies rclpy

where in this case the lonely dependency is the one for the ROS2 python library rclpy. This create the following file tree:

/workspace/src/my_package
├── my_package
│   └── __init__.py
├── package.xml
├── resource
│   └── my_package
├── setup.cfg
├── setup.py
└── test
    ├── test_copyright.py
    ├── test_flake8.py
    └── test_pep257.py

Executable scrips must be placed inside the folder with the same name of the initialized package (in this case my_package) where an empty __init__.py is already present.

Launch files integration

Since launch files will more than likely to be required for a new package, it can be useful to create a launch folder in which to store such entries; once in the root directory of the packages

mkdir launch

To automatically copy launch files during package builds, update the setup.py file as shown here

# In the import section
import os
from glob import glob

# Later in the file
data_files=[
    ('share/ament_index/resource_index/packages',    # already present
        ['resource/' + package_name]),
    ('share/' + package_name, ['package.xml'])       # already present,
    (os.path.join('share', package_name), glob('launch/*.launch.py')),
],

If particular data_files is a list of tuple. Each tuple contains the destination path on which data will be copied (relative to the install space of the ROS workspace), while the second element is a list of files to be copied (relative to the package source root). In this particular case we glob all launch files that are included in the launch folder.

Adding executables

As previously mentioned, .py executables must be inserted inside the folder with the same name of the package.

As good practise, its recommended to include the main inclusion guard for the python executable. The barebone of a script could be

#!/usr/bin/env python3
# imports


def main(args=None):
    # main function
    pass


if __name__ == '__main__':
    main()

A fundamental step is to add the executable to the setup.py file, in the entry_points section as follows

setup(
    # ...
    entry_points={
        'console_scripts': [
            '<executable_name> = <package_name>.<script_name>:main',
        ],
    },
    # ...
)

In particular the <executable_name> is the name of the executable that needs to be called in the launch file <script_name> is the name of the script file that has been developed (and do not necessarily needs to be the same as the executable name).

C++ package creation

C++ packages can be create similarly to python ones just by changing the build type

ros2 pkg create --build-type ament_cmake <package_name> --dependencies <dependencies>

In this case the resulting package structure is different:

/workspace/src/<package_name>
├── CMakeLists.txt
├── include
│   └── <package_name>
├── package.xml
└── src

Applications (.cpp files) should be developed inside the src folder, while the include folder should contain the header files.

CMake integration

Once source code is developed, the CMake file must be updated in order to correctly build and install the targets. Bare example to add:

# Declare your CMake version
cmake_minimum_required(VERSION 3.5)
# Declare project's name (the same of the one in the package.xml)
project(simple_node_pkg_name)

# Exploit CMake's macros to find the required dependencies
# This is required by colcon
find_package(ament_cmake REQUIRED)
# This will tell CMake to find the rclcpp library in your OS
find_package(rclcpp REQUIRED)
# If you need to use some specific ROS2 msg (for example sensor_msgs)
find_package(sensor_msgs REQUIRED)
# If you need other libraries like eigen you have to find them too
find_package (Eigen3 3.3 REQUIRED NO_MODULE)

# Initialise the executable my_node with the source code simple_node.cpp
add_executable(my_node src/simple_node.cpp)

# If you put some headers inside the 'include' folders, then you have to import them.
target_include_directories(my_node include)

# Link the dependencies to your node
ament_target_dependencies(my_node rclcpp sensor_msgs Eigen3::Eigen)

install( # copy target to the lib folder inside the install space
   TARGETS my_node
   DESTINATION lib/${PROJECT_NAME}
)

install( # copy launch files to the share folder inside the install space
   DIRECTORY launch
   DESTINATION share/${PROJECT_NAME}
)

ament_package()

Note

The name of the target is the one that should be used as executable name in the launch file. It is assumed that launch files are placed inside the launch folder and the install directive must be inserted to copy them to the install space.

Mixed package creation (C++ and Python)

There is also the possibility to extend a C++ package in order to run also Python nodes. In order to do it you have to create a scripts folder inside your package. Then modify the CMake accordingly:

# Find the ROS2 Python package
find_package(rclpy REQUIRED)

#...

# Install Python modules
ament_python_install_package(${PROJECT_NAME})
# Install Python executables
install(PROGRAMS
scripts/your_python_node_name.py
DESTINATION lib/${PROJECT_NAME}
)

You also need to update the package.xml:

<!-- ... -->
<depend>rclpy</depend>
<!-- ... -->
<buildtool_depend>ament_cmake_python</buildtool_depend>

Launch files

Launch files can be used to start multiple nodes at once from the same script. They are usually placed inside the launch directory of the package, and files are named <file_name>.launch.<format> where the format can be either py, yaml or xml. The complete official documentation can be found here.

Python based launch files are as follows:

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package="<package_name>",
            executable="<executable_name>",
            output="screen"),
    ])

The LaunchDescription takes as input a list of nodes: that’s how multiple of them can be started.