8
Client Libraries

8.1 Getting Started with Colcon

Now we have learned previously all the essential concepts required for us to get a good introduction to ROS and now we can begin to go deeper into understanding how packages are written and how all previously defined concepts and ideas can be implemented in code. In this light, let us start with looking at the compiler for ROS : colcon

a meta build tool to improve the workflow of building, testing and using multiple software packages [42].

The first thing to define is colcon is an iteration on the ROS build tools:

catkin_make, catkin_make_isolated, catkin_tools and ament_tools.

We begin by installing colcon: 1 1 Of course, if we have installed ROS with a Dockerfile and followed the lecture material, we don’t have to do this step. However, this step is nevertheless here in case the document is followed non-linearly.

bash
sudo apt install python3-colcon-common-extensions
text
Reading package lists... 0% Reading package lists... 0% Reading package lists... 7% Reading package lists... Done Building dependency tree... 0% Building dependency tree... 0% Building dependency tree... 50% Building dependency tree... 50% Building dependency tree... Done Reading state information... 0% Reading state information... 0% Reading state information... Done python3-colcon-common-extensions is already the newest version (0.3.0-100). 0 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.

The Structure of a Package A ROS workspace is a directory with a particular structure . Commonly there is a src subdirectory. 2 2 This kind of directory usually exists for development of applications such as C or C++. Within this subdirectory is where the source code (src) of ROS packages will be located.

When this directory is created, it will be empty.

The primary job of colcon is to create the structure for, compile and build these source files. By default it will create the following directories as same hierarchy of the src directory:

build

Where intermediate files are stored. For each package a subfolder will be created in which e.g., CMake 3 3 a free, cross-platform, software development tool for building applications via compiler-independent instructions and allows automate testing, packaging and installation. It runs on a variety of platforms and supports many programming languages. is being invoked.

install

Where each package will be installed to. By default each package will be installed into a separate subdirectory.

log

Contains various logging information about each colcon invocation, for use in error-checking and debugging purposes.

For any student who worked with ROS 1 and catkin, there is no devel directory.

To get started we first create a directory (ros2\_ws) to contain our workspace: 4 4 The name of the directory is up to the user.

bash
mkdir -p ~/ros2_ws/src cd ~/ros2_ws

At this moment if we were to ls into our directory we will only see one (1) folder which is src, which is to be expected as it is created just a second ago.

bash
ls
text
src

Now let’s populate our newly created environment with some tutorial files from the official ROS repo:

bash
git clone https://github.com/ros2/examples src/examples -b humble
text
Cloning into 'src/examples'... remote: Enumerating objects: 9987, done. remote: Counting objects: 100% (2546/2546), done. remote: Compressing objects: 100% (358/358), done. remote: Total 9987 (delta 2376), reused 2195 (delta 2188), pack-reused 7441 (from 3) Receiving objects: 100% (9987/9987), 1.56 MiB | 4.28 MiB/s, done. Resolving deltas: 100% (7250/7250), done.

Sourcing an Underlay It is important that we’ve sourced the environment for an existing ROS installation which will provide our workspace with the necessary build dependencies for the example packages. This is achieved by sourcing the setup script provided by a binary installation or a source installation. 5 5 i.e. another, colcon workspace.

We call this environment an underlay .

Our workspace, ros2\_ws, will be an overlay on top of the existing ROS installation.

In general, it is recommended to use an overlay when we plan to iterate on a small number of packages, rather than putting all of our packages into the same workspace.

Building the Workspace In the root of the workspace, run colcon build. Since build types such as ament_cmake do NOT support the concept of the devel space and require the package to be installed, colcon supports the option --symlink-install. This allows the installed files to be changed by changing the files in the source space 6 6 e.g., Python files or other non-compiled resources for faster iteration.

bash
colcon build --symlink-install --executor sequential

The way we are currently building is NOT the official way as we had to add --executor sequential option. We are adding this as by default, the colcon processes/compiles packages parallel to speed up the compilation time. If we were to have ROS installed on native hardware rather than a docker container we may not have needed this additional option.

After the build is finished, we should see the build, install, and log directories:

bash
ls -l
text
drwxr-xr-x 24 ubuntu ubuntu 4096 Jun 6 13:58 build drwxr-xr-x 24 ubuntu ubuntu 4096 Jun 6 13:58 install drwxr-xr-x 4 ubuntu ubuntu 4096 Jun 6 13:57 log drwxr-xr-x 3 ubuntu ubuntu 4096 Jun 6 13:54 src

To run test on built packages we can run colcon test

Sourcing the New Package When colcon has completed building successfully, the output will be in the install directory. Before we can use any of the installed executables or libraries, we will have to add them to our path and library paths. colcon will have generated bash files in the install directory to help set up the environment. These files will add all of the required elements to our path and library paths as well as provide any bash or shell commands exported by packages.

bash
source install/setup.bash

It is time to test out what we have built. Let’s open up a new terminal window side by side and run the following two (2) commands:

bash
ros2 run examples_rclcpp_minimal_subscriber subscriber_member_function

bash
ros2 run examples_rclcpp_minimal_publisher publisher_member_function

If everything has worked well, we should see messages from the publisher and subscriber with numbers incrementing.

Information : Overlay v. Underlay

As a final clarification on these two (2) concepts, let’s look at them in a bit more detail:

  • An underlay is the core ROS installation that provides the foundational packages and environment for your ROS development. In our case it is Humble.

  • An overlay is the secondary workspace where we can add new packages without interfering with the existing ROS 2 workspace that we’re extending.

8.2 Creating a Workspace

A workspace is a directory containing ROS packages. Before using ROS , it’s necessary to source our ROS installation workspace in the terminal we plan to work in. This makes ROS ’s packages available for you to use in that terminal.

We also have the option of sourcing an “overlay”, which is a secondary workspace where we can add new packages without interfering with the existing ROS workspace that we’re extending, or “underlay”.

Our underlay must contain the dependencies of all the packages in our overlay.

Packages in our overlay will override packages in the underlay.

It’s also possible to have several layers of underlays and overlays, with each successive overlay using the packages of its parent underlays.

Sourcing the Environment Our main ROS installation will be our underlay for this section. 7 7 Keep in mind that an underlay does NOT necessarily have to be the main ROS installation.

Depending on how we installed ROS , either from source or binaries, and which platform we’re on, our exact source command will vary:

bash
source /opt/ros/humble/setup.bash

Creating a New Directory Best practice is to create a new directory for every new workspace . The name doesn’t matter, but it is helpful to have it indicate the purpose of the workspace. Let’s choose the directory name ros2\_ws, for “development workspace”:

bash
mkdir -p ~/ros2_ws/src cd ~/ros2_ws/src

Another best practice is to put any packages in our workspace into the src directory. The above code creates a src directory inside ros2\_ws and then navigates into it.

Cloning a Sample Repo Ensure we’re still in the ros2_ws/src directory before we clone.

In the following sections, we will create our own packages, but for now we will practice putting a workspace together using existing packages. A repo can have multiple branches. We need to check out the one that targets our installed ROS distro. When we clone this repo, add the -b argument followed by that branch.

In the ros2\_ws/src directory, run the following command:

bash
git clone https://github.com/ros/ros_tutorials.git -b humble

Now ros_tutorials is cloned in our workspace. The ros_tutorials repository contains the turtlesim package, which we’ll use in this section. The other packages in this repository are not built because they contain a COLCON_IGNORE file.

So far we have populated our workspace with a sample package, but it isn’t a fully-functional workspace yet as we need to resolve the dependencies first and then build the workspace.

Resolving Dependencies Before building the workspace, we need to resolve the package dependencies. It is possible we may have all the dependencies already, but best practice is to check for dependencies every time we clone. We wouldn’t want a build to fail after a long wait only to realize that we have missing dependencies.

From the root of our workspace (ros2\_ws), run the following command:

If we’re still in the src directory with the ros_tutorials clone, make sure to run cd .. to move back up to the workspace (ros2\_ws).

bash
cd .. rosdep install -i --from-path src --rosdistro humble -y

If we already have all our dependencies, the console will return:

text
#All required rosdeps installed successfully

Packages declare their dependencies in the package.xml file. 8 8 We will learn more about packages in the following section. This command walks through those declarations and installs the ones that are missing.

Building the Workspace From the root of our workspace (ros2\_ws), we can now build our packages using the command:

bash
colcon build
text
Starting »> turtlesim Finished «< turtlesim [5.49s] Summary: 1 package finished [5.58s]

There are some useful arguments for colcon build which are as follows:

--packages-up-to

builds the package we want, plus all its dependencies, but not the whole workspace (saves time)

--symlink-install

saves us from having to rebuild every time we tweak python scripts

--event-handlers console_direct+

shows console output while building (can otherwise be found in the log directory)

--executor sequential

processes the packages one by one instead of using parallelism

Once the build is finished, enter the command in the workspace root (~/ros2_ws). We will see that colcon has created new directories:

bash
ls
text
build install log src

The install directory is where our workspace’s setup files are, which we can use to source our overlay.

Sourcing our Overlay Before sourcing the overlay, it is very important that we open a new terminal, separate from the one where we built the workspace. Sourcing an overlay in the same terminal where we built, or likewise building where an overlay is sourced, may create complex issues.

In the new terminal, source our main ROS 2 environment as the “underlay”, so we can build the overlay “on top of” it:

bash
source /opt/ros/humble/setup.bash

Time to go into the root of our workspace:

bash
cd ~/ros2_ws

In the root, source our overlay:

bash
source install/local_setup.bash

Sourcing the local_setup of the overlay will only add the packages available in the overlay to our environment. setup sources the overlay as well as the underlay it was created in, allowing us to utilise both workspaces. So, sourcing our main ROS installation’s setup and then the ros2\_ws overlay’s local_setup, like we just did, is the same as just sourcing ros2\_ws ’s setup, because that includes the environment of its underlay.

Now we can run the turtlesim package from the overlay:

bash
ros2 run turtlesim turtlesim_node

But how can you tell that this is the overlay turtlesim running, and not your main installation’s turtlesim?

Let’s modify turtlesim in the overlay so you can see the effects:

  • We can modify and rebuild packages in the overlay separately from the underlay.

  • The overlay takes precedence over the underlay.

Modifying the Overlay You can modify turtlesim in your overlay by editing the title bar on the turtlesim window. To do this, locate the turtle\_frame.cpp file in [ /] /ros2_ws/src/ros_tutorials/turtlesim/src. Open turtle\_frame.cpp with your preferred text editor.

Find the function setWindowTitle("TurtleSim");, change the value " TurtleSim " to " MyTurtleSim ", and save the file.

Return to the first terminal where you ran colcon build earlier and run it again.

bash
ros2 run turtlesim turtlesim_node

Return to the second terminal (where the overlay is sourced) and run turtlesim again:

Even though your main ROS 2 environment was sourced in this terminal earlier, the overlay of your ros2\_ws environment takes precedence over the contents of the underlay.

To see that your underlay is still intact, open a brand new terminal and source only your ROS 2 installation. Run turtlesim again:

bash
ros2 run turtlesim turtlesim_node

You can see that modifications in the overlay did not actually affect anything in the underlay.

8.3 Creating a Package

A package is an organizational unit for our ROS code. If we want to be able to install our code or share it with others, then we’ll need it organized in a package. With packages, we can release our ROS work and allow others to build and use it easily.

Package creation in ROS uses ament as its build system and colcon as its build tool. We can create a package using either CMake or Python, which are officially supported, though other build types do exist.

The Anatomy of a Package ROS Python and CMake packages each have their own minimum required contents:

package.xml

file containing meta information about the package

resource/

marker file for the package

setup.cfg

is required when a package has executables, so ros2 run can find them

setup.py

containing instructions for how to install the package

a directory with the same name as our package, used by ROS tools to find our package, contains __init__.py

The simplest possible package may have a file structure that looks like:

8.4 Writing a Simple Publisher & Subscriber

In this exercise, we will create nodes which passes information in the form of string messages to each other over a topic . The example we will use here is a simple “talker” and “listener” system where

one publishes data and the other subscribes to the topic so it can receive that data.

Creating a ROS Packages To begin, we open a new terminal window and navigate to our previously created ros2\_ws.

Recall that packages should be created in the src directory, not the root / of the workspace.

So naturally, navigate into the ros2_ws/src directory, and run the package creation command:

bash
ros2 pkg create --build-type ament_python --license Apache-2.0 py_pubsub

If everything works well, our terminal will return a message verifying the creation of our package py_pubsub and all its necessary files and folders.

8.4.1 Writing the Publisher Node

Now that we have a package template, please navigate into ros2_ws/src/py_pubsub/py_pubsub. To get started, download the example talker code by entering the following command:

bash
wget https://raw.githubusercontent.com/ros2/examples/humble/rclpy/topics/minimal_publisher/examples_rclpy_minimal_publisher/publisher_member_function.py

Here the wget command basically access the file in a web-server and then downloads it to the current working directory. If the code works successfully, there will be a new file named publisher_member_function.py adjacent to __init__.py.

Now let’s open the code and see what is going on under the hood. The following is the code in full with snippets and detailed explanation to follow:

python
import rclpy from rclpy.node import Node from std_msgs.msg import String class MinimalPublisher(Node): def __init__(self): super().__init__('minimal_publisher') self.publisher_ = self.create_publisher(String, 'topic', 10) timer_period = 0.5 # seconds self.timer = self.create_timer(timer_period, self.timer_callback) self.i = 0 def timer_callback(self): msg = String() msg.data = 'Hello World: %d' % self.i self.publisher_.publish(msg) self.get_logger().info('Publishing: "%s"' % msg.data) self.i += 1 def main(args=None): rclpy.init(args=args) minimal_publisher = MinimalPublisher() rclpy.spin(minimal_publisher) # Destroy the node explicitly # (optional - otherwise it will be done automatically # when the garbage collector destroys the node object) minimal_publisher.destroy_node() rclpy.shutdown() if __name__ == '__main__': main()

Deconstructing the Code The first lines of code after the comments import rclpy so its Node class can be used. 9 9 As a reminder, the rclpy is the ROS library written for Python.

python
import rclpy from rclpy.node import Node

The next statement imports the built-in string message type which the node uses to structure the data it passes on the topic.

python
from std_msgs.msg import String

These aforementioned lines represent the node’s dependencies . Recall that dependencies have to be added to package.xml, which we will have a look at in just a little bit. Next, the MinimalPublisher class is created, which inherits 10 10 or is a subclass of from Node.

python
class MinimalPublisher(Node):

Following is the definition of the class’s constructor. super().__init__ calls the Node class’s constructor and gives it our node name, in this case minimal_publisher.

create_publisher declares the node publishes messages of type String, 11 11 which is imported from the std_msgs.msg module over a topic named topic, and that the “queue size” is 10.

Queue size is a required QoS (quality of service) setting which limits the amount of queued messages if a subscriber is NOT receiving them fast enough.

Next, a timer is created with a callback to execute every 0.5 seconds. self.i is a counter used in the callback.

python
def __init__(self): super().__init__('minimal_publisher') self.publisher_ = self.create_publisher(String, 'topic', 10) timer_period = 0.5 # seconds self.timer = self.create_timer(timer_period, self.timer_callback) self.i = 0

timer_callback creates a message with the counter value appended, and publishes it to the console with get_logger().info.

python
def timer_callback(self): msg = String() msg.data = 'Hello World: %d' % self.i self.publisher_.publish(msg) self.get_logger().info('Publishing: "%s"' % msg.data) self.i += 1

Lastly, the main function is defined.

python
def main(args=None): rclpy.init(args=args) minimal_publisher = MinimalPublisher() rclpy.spin(minimal_publisher) # Destroy the node explicitly # (optional - otherwise it will be done automatically # when the garbage collector destroys the node object) minimal_publisher.destroy_node() rclpy.shutdown()

First the rclpy library is initialized, then the node is created, and then it “spins” the node so its callbacks are called.

Adding Dependencies Navigate one level back to the ros2_ws/src/py_pubsub directory, where the setup.py, setup.cfg, and package.xml files have been created for us. Open package.xml with our favourite text editor. 12 12 This could of course be emacs, or something which is not emacs so we can see why emacs is better.

As mentioned previously, make sure to fill in the <description>, <maintainer> and <license> tags:

xml
<description>Examples of minimal publisher/subscriber using rclpy</description> <maintainer email="you@email.com">Your Name</maintainer> <license>Apache License 2.0</license>

After the lines above, add the following dependencies corresponding to our node’s import statements: 13 13 Remember, we need to let ROS know the dependencies required by the python script.

xml
<exec_depend>rclpy</exec_depend> <exec_depend>std_msgs</exec_depend>

This declares the package needs rclpy and std_msgs when its code is executed.

Adding An Entry Point Given we have sorted our package manifesto, we need to configure our python code. Open the setup.py file. Again, match the maintainer, maintainer_email, description and license fields to our package.xml :

python
maintainer='YourName', maintainer_email='you@email.com', description='Examples of minimal publisher/subscriber using rclpy', license='Apache License 2.0',

Add the following line within the console_scripts brackets of the entry_points field:

python
entry_points={ 'console_scripts': [ 'talker = py_pubsub.publisher_member_function:main', ], },

Checking the Configuration File The contents of the setup.cfg file should be correctly populated automatically, like so:

cfg
[develop] script_dir=$base/lib/py_pubsub [install] install_scripts=$base/lib/py_pubsub

This code is simply telling setuptools to put our executables in lib, because ros2 run will look for them there. We could build our package now, source the local setup files, and run it, but let’s create the subscriber node first so we can see the full system at work.

8.4.2 Writing the Subscriber Node

Return to ros2_ws/src/py_pubsub/py_pubsub to create the next node. Enter the following code in our terminal:

bash
wget https://raw.githubusercontent.com/ros2/examples/humble/rclpy/topics/minimal_subscriber/examples_rclpy_minimal_subscriber/subscriber_member_function.py

Now the directory should have these files:

__init__.py, publisher_member_function.py and subscriber_member_function.py

Examining the Code Open the subscriber_member_function.py with our preferred text editor.

python
import rclpy from rclpy.node import Node from std_msgs.msg import String class MinimalSubscriber(Node): def __init__(self): super().__init__('minimal_subscriber') self.subscription = self.create_subscription( String, 'topic', self.listener_callback, 10) self.subscription # prevent unused variable warning def listener_callback(self, msg): self.get_logger().info('I heard: "%s"' % msg.data) def main(args=None): rclpy.init(args=args) minimal_subscriber = MinimalSubscriber() rclpy.spin(minimal_subscriber) # Destroy the node explicitly # (optional - otherwise it will be done automatically # when the garbage collector destroys the node object) minimal_subscriber.destroy_node() rclpy.shutdown() if __name__ == '__main__': main()

The subscriber node’s code is nearly identical to the publisher’s. The constructor creates a subscriber with the same arguments as the publisher.

the topic name and message type used by the publisher and subscriber must match to allow them to communicate.

python
self.subscription = self.create_subscription( String, 'topic', self.listener_callback, 10)

The subscriber’s constructor and callback don’t include any timer definition, because it doesn’t need one. Its callback gets called as soon as it receives a message.

The callback definition simply prints an info message to the console, along with the data it received. Recall that the publisher defines

python
def listener_callback(self, msg): self.get_logger().info('I heard: "%s"' % msg.data)

The main definition is almost exactly the same, replacing the creation and spinning of the publisher with the subscriber.

python
minimal_subscriber = MinimalSubscriber() rclpy.spin(minimal_subscriber)

Since this node has the same dependencies as the publisher, there’s nothing new to add to package.xml. The setup.cfg file can also remain untouched.

Adding an Entry Point Reopen setup.py and add the entry point for the subscriber node below the publisher’s entry point. The entry_points field should now look like this:

python
entry_points={ 'console_scripts': [ 'talker = py_pubsub.publisher_member_function:main', 'listener = py_pubsub.subscriber_member_function:main', ], },

8.4.3 Building and Running

We likely already have the rclpy and std_msgs packages installed as part of our ROS system. It’s good practice to run rosdep in the root of our workspace (ros2\_ws) to check for missing dependencies before building:

8.5 Writing a Simple Service and Client

When nodes communicate using services, the node which sends a request for data is called the client node, and the one that responds to the request is the service node. The structure of the request and response is determined by a .srv file.

The example used here is a simple integer addition system;

one node requests the sum of two (2) integers, and the other responds with the result.

Now let’s write our implementation.

Creating a New Package Let’s begin by opening a new terminal and source our ROS installation so ros2 commands will work. Once done, please navigate into the ros2\_ws directory created in previously.

Remember that, packages should be created in the src directory and NOT the root of the workspace. Navigate into ros2_ws/src and create a new package:

bash
ros2 pkg create \ --build-type ament_python \ --license Apache-2.0 py_srvcli \ --dependencies rclpy example_interfaces

Our terminal will return a message verifying the creation of our package py_srvcli and all its necessary files and folders.

The --dependencies argument will automatically add the necessary dependency lines to package.xml. example_interfaces is the package that includes the .srv file we will need to structure our requests and responses:

xml
int64 a int64 b --- int64 sum

The first two lines are the parameters of the request, and below the dashes is the response.

Updating Package Manifesto Because we used the --dependencies option during package creation, we don’t have to manually add dependencies to package.xml.

As always, though, make sure to add the description, maintainer email and name, and license information to package.xml as a good open-source developer.

xml
<description>Python client server tutorial</description> <maintainer email="you@email.com">Your Name</maintainer> <license>Apache License 2.0</license>

Updating the Configuration Add the same information to the setup.py file for the maintainer, maintainer_email, description and license fields:

python
maintainer='Your Name', maintainer_email='you@email.com', description='Python client server tutorial', license='Apache License 2.0',

8.5.1 Writing the Service Node

Once we are sure we are inside the [ /]ros2_ws/src/py_srvcli/py_srvcli directory, we then create a new file called service\_member\_function.py and paste the following code within:

python
from example_interfaces.srv import AddTwoInts import rclpy from rclpy.node import Node class MinimalService(Node): def __init__(self): super().__init__('minimal_service') self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback) def add_two_ints_callback(self, request, response): response.sum = request.a + request.b self.get_logger().info('Incoming request\na: %d b: %d' % (request.a, request.b)) return response def main(): rclpy.init() minimal_service = MinimalService() rclpy.spin(minimal_service) rclpy.shutdown() if __name__ == '__main__': main()

Let’s look at the code in more detail and what what is going on.

Examining the Code The first import statement imports the AddTwoInts service type from the example_interfaces package. The following import statement imports the ROS Python client library (rclpy), and specifically the Node class.

python
from example_interfaces.srv import AddTwoInts import rclpy from rclpy.node import Node

The MinimalService class constructor initializes the node with the name minimal_service. Then, it creates a service and defines the type, name, and callback.

python
def __init__(self): super().__init__('minimal_service') self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback)

The definition of the service callback receives the request data, sums it, and returns the sum as a response.

python
def add_two_ints_callback(self, request, response): response.sum = request.a + request.b self.get_logger().info('Incoming request\na: %d b: %d' % (request.a, request.b)) return response

Finally, the main class initializes the ROS Python client library, instantiates the MinimalService class to create the service node and spins the node to handle callbacks.

Adding an Entry Point To allow the ros2 run command to run our node, we must add the entry point to setup.py. 14 14 This is located in the [ /]ros2_ws/src/py_srvcli directory. To make this happen, all we have to do this add the following line between the 'console_scripts': brackets:

python
'service = py_srvcli.service_member_function:main',

8.5.2 Writing the Client Node

Once we are inside the [ /]ros2_ws/src/py_srvcli/py_srvcli directory, create a new file called client\_member\_function.py and paste the following code within:

python
import sys from example_interfaces.srv import AddTwoInts import rclpy from rclpy.node import Node class MinimalClientAsync(Node): def __init__(self): super().__init__('minimal_client_async') self.cli = self.create_client(AddTwoInts, 'add_two_ints') while not self.cli.wait_for_service(timeout_sec=1.0): self.get_logger().info('service not available, waiting again...') self.req = AddTwoInts.Request() def send_request(self, a, b): self.req.a = a self.req.b = b return self.cli.call_async(self.req) def main(): rclpy.init() minimal_client = MinimalClientAsync() future = minimal_client.send_request(int(sys.argv[1]), int(sys.argv[2])) rclpy.spin_until_future_complete(minimal_client, future) response = future.result() minimal_client.get_logger().info( 'Result of add_two_ints: for %d + %d = %d' % (int(sys.argv[1]), int(sys.argv[2]), response.sum)) minimal_client.destroy_node() rclpy.shutdown() if __name__ == '__main__': main()

Examining the Code As with the service code, we first import the necessary libraries.

python
import sys from example_interfaces.srv import AddTwoInts import rclpy from rclpy.node import Node

The MinimalClientAsync class constructor initializes the node with the name minimal_client_async. The constructor definition creates a client with the same type and name as the service node. The type and name must match for the client and service to be able to communicate. The while loop in the constructor checks if a service matching the type and name of the client is available once a second. Finally it creates a new AddTwoInts request object.

python
def __init__(self): super().__init__('minimal_client_async') self.cli = self.create_client(AddTwoInts, 'add_two_ints') while not self.cli.wait_for_service(timeout_sec=1.0): self.get_logger().info('service not available, waiting again...') self.req = AddTwoInts.Request()

Below the constructor is the send_request method, which will send the request and spin until it receives the response or fails.

python
def send_request(self, a, b): self.req.a = a self.req.b = b return self.cli.call_async(self.req)

Finally we have the main method, which constructs a MinimalClientAsync object, sends the request using the passed-in command-line arguments, calls rclpy.spin_until_future_complete to wait for the result, and logs the results.

Adding an Entry Point Similar to the service node, we also have to add an entry point to be able to run the client node from the command-line. The entry_points field of our setup.py file should look like this:

python
entry_points={ 'console_scripts': [ 'service = py_srvcli.service_member_function:main', 'client = py_srvcli.client_member_function:main', ], },

It’s good practice to run rosdep in the root of our workspace (ros2\_ws) to check for missing dependencies before building:

python
rosdep install -i --from-path src --rosdistro humble -y

Navigate back to the root of our workspace, ros2\_ws, and build our new package:

python
colcon build --packages-select py_srvcli

Open a new terminal, navigate to ros2\_ws, and source the setup files:

python
source install/setup.bash

Now run the service node:

python
ros2 run py_srvcli service

The node will wait for the client’s request.

Open another terminal and source the setup files from inside ros2\_ws again. Start the client node, followed by any two integers separated by a space. If we chose 2 and 3, for example, the client would receive a response like this:

python
ros2 run py_srvcli client 2 3
text
[INFO] [minimal_client_async]: Result of add_two_ints: for 2 + 3 = 5

Return to the terminal where our service node is running. We will see that it published log messages when it received the request:

text
[INFO] [minimal_service]: Incoming request a: 2 b: 3

8.6 Creating Custom msg and srv Files

Previously, we utilised message and service interfaces to learn about:

  • topics.

  • services,

  • simple publisher/subscriber, and

  • service/client nodes.

It is worth noting, the interfaces we used previously were predefined in those cases.

While it’s good practice to use predefined interface definitions, time will eventually come, where we will need to define our own messages and services sometimes as well. Here, we will have a look at the simplest method of creating custom interface definitions.

Creating a New Package Here, we will be creating our custom .msg and .srv files in their own package, and then utilising them in a separate package.

It is worth stressing, that both packages should be in the same workspace.

As we will use the pub/sub and service/client packages we created previously, make sure we are in the same workspace as those packages (ros2_ws/src), and then run the following command to create a new package:

bash
ros2 pkg create --build-type ament_cmake --license Apache-2.0 tutorial_interfaces

tutorial_interfaces is the name of the new package . Note that it is, and can only be, an ament_cmake package, 15 15 It is basically a build system for CMake based packages in ROS . It is a set of scripts enhancing CMake and adding convenience functionality for package authors. but this doesn’t restrict in which type of packages we can use our messages and services. We can create our own custom interfaces in an ament_cmake package, and then use it in a C++ or Python node, which will be covered later.

The .msg and .srv files are required to be placed in directories called msg and srv respectively.

We can create the directories in [ /]ros2_ws/src/tutorial_interfaces using:

bash
mkdir msg srv

8.6.1 Creating Custom Definitions

msg Definition In the [ /]tutorial_interfaces/msg directory we just created, make a new file called Num.msg with one line of code declaring its data structure:

text
int64 num

This is a custom message which transfers a single 64-bit integer called num.

In addition, in the tutorial\_interfaces/msg directory we’ve just created, make a new file called Sphere.msg with the following content:

bash
geometry_msgs/Point center float64 radius

This custom message uses a message from another message package. As we can see from the first line in the message, it is geometry_msgs/Point.

srv Definition Let’s go back in the tutorial_interfaces/srv directory we’ve just created a few moment ago and make a new file called AddThreeInts.srv with the following request and response structure:

bash
int64 a int64 b int64 c --- int64 sum

This is our custom service which requests three (3) integers aptly named a, b, and c, and responds with an integer called sum.

CMakeLists To convert the interfaces we defined into language-specific code 16 16 This can be either C++ or Python. so that they can be used in those languages, let’s add the following lines to our CMakeLists.txt :

cmake
find_package(geometry_msgs REQUIRED) find_package(rosidl_default_generators REQUIRED) rosidl_generate_interfaces(${PROJECT_NAME} "msg/Num.msg" "msg/Sphere.msg" "srv/AddThreeInts.srv" DEPENDENCIES geometry_msgs # Add packages that above messages #depend on, in this case geometry_msgs for Sphere.msg )

The first argument (library name) in the rosidl_generate_interfaces must start with the name of the package, e.g., simply ${PROJECT_NAME} or ${PROJECT_NAME}_suffix.

Package.xml Because the interfaces rely on rosidl_default_generators for generating language-specific code, we need to declare a build tool dependency on it. rosidl_default_runtime is a runtime or execution-stage dependency, needed to be able to use the interfaces later.

The rosidl_interface_packages is the name of the dependency group which our package, tutorial_interfaces, should be associated with, declared using the <member_of_group> tag.

Add the following lines within the <package> element of package.xml :

xml
<depend>geometry_msgs</depend> <buildtool_depend>rosidl_default_generators</buildtool_depend> <exec_depend>rosidl_default_runtime</exec_depend> <member_of_group>rosidl_interface_packages</member_of_group>

Please pay attention to the last line where we have added the new tag.

Building the Package Now that all the parts of our custom interfaces package are in place, we can finally build the package. In the root of our workspace (~/ros2_ws), please run the following command:

bash
colcon build --packages-select tutorial_interfaces

Now the interfaces will be discoverable by other ROS packages.

Confirming the Creation In a new terminal, let’s run the following command from within our workspace (ros2\_ws) to source it if it is required:

bash
source install/setup.bash

Now we can confirm our interface creation has worked by using the ros2 interface show command. The output we see in our terminal should look similar to the following:

bash
ros2 interface show tutorial_interfaces/msg/Num
text
int64 num

bash
ros2 interface show tutorial_interfaces/msg/Sphere
text
geometry_msgs/Point center float64 x float64 y float64 z float64 radius

bash
ros2 interface show tutorial_interfaces/srv/AddThreeInts
text
int64 a int64 b int64 c --- int64 sum

8.6.2 Testing the Newly Built Interfaces

Time to see our new interface in actiony. A few simple modifications to the nodes, CMakeLists.txt and package.xml files will allow we to use our new interfaces.

Publisher and Subscriber System: Publisher Code

python
import rclpy from rclpy.node import Node from tutorial_interfaces.msg import Num # CHANGE class MinimalPublisher(Node): def __init__(self): super().__init__('minimal_publisher') self.publisher_ = self.create_publisher(Num, 'topic', 10) # CHANGE timer_period = 0.5 self.timer = self.create_timer(timer_period, self.timer_callback) self.i = 0 def timer_callback(self): msg = Num() # CHANGE msg.num = self.i # CHANGE self.publisher_.publish(msg) self.get_logger().info('Publishing: "%d"' % msg.num) # CHANGE self.i += 1 def main(args=None): rclpy.init(args=args) minimal_publisher = MinimalPublisher() rclpy.spin(minimal_publisher) minimal_publisher.destroy_node() rclpy.shutdown() if __name__ == '__main__': main()
Publisher and Subscriber System: Subscriber Code

python
import rclpy from rclpy.node import Node from tutorial_interfaces.msg import Num # CHANGE class MinimalSubscriber(Node): def __init__(self): super().__init__('minimal_subscriber') self.subscription = self.create_subscription( Num, # CHANGE 'topic', self.listener_callback, 10) self.subscription def listener_callback(self, msg): self.get_logger().info('I heard: "%d"' % msg.num) # CHANGE def main(args=None): rclpy.init(args=args) minimal_subscriber = MinimalSubscriber() rclpy.spin(minimal_subscriber) minimal_subscriber.destroy_node() rclpy.shutdown() if __name__ == '__main__': main()

We also need to edit our package.xml file to make all the previous python code to work.

xml
<exec_depend>tutorial_interfaces</exec_depend>

Once we have done the necessary changes let’s build our package and execute it.

bash
colcon build --packages-select py_pubsub

Then open two new terminals, source ros2\_ws in each, and run:

bash
ros2 run py_pubsub talker

bash
ros2 run py_pubsub talker

Since Num.msg relays only an integer, the talker should only be publishing integer values, as opposed to the string it published previously:

text
[INFO] [minimal_publisher]: Publishing: '0' [INFO] [minimal_publisher]: Publishing: '1' [INFO] [minimal_publisher]: Publishing: '2'
Service Client System

With a few modifications to the service/client package created previously, we can see AddThreeInts.srv in action. Since we’ll be changing the original two (2) integer request srv to a three (3) integer request srv, the output will be slightly different.

Service

python
from tutorial_interfaces.srv import AddThreeInts # CHANGE import rclpy from rclpy.node import Node class MinimalService(Node): def __init__(self): super().__init__('minimal_service') self.srv = self.create_service(AddThreeInts, 'add_three_ints', self.add_three_ints_callback) # CHANGE def add_three_ints_callback(self, request, response): response.sum = request.a + request.b + request.c # CHANGE self.get_logger()\ .info('Incoming request\na: %d b: %d c: %d' % (request.a, request.b, request.c)) # CHANGE return response def main(args=None): rclpy.init(args=args) minimal_service = MinimalService() rclpy.spin(minimal_service) rclpy.shutdown() if __name__ == '__main__': main()
Client

python
from tutorial_interfaces.srv import AddThreeInts # CHANGE import sys import rclpy from rclpy.node import Node class MinimalClientAsync(Node): def __init__(self): super().__init__('minimal_client_async') self.cli = self.create_client(AddThreeInts, 'add_three_ints') # CHANGE while not self.cli.wait_for_service(timeout_sec=1.0): self.get_logger().info('service not available, waiting again...') self.req = AddThreeInts.Request() # CHANGE def send_request(self): self.req.a = int(sys.argv[1]) self.req.b = int(sys.argv[2]) self.req.c = int(sys.argv[3]) # CHANGE self.future = self.cli.call_async(self.req) def main(args=None): rclpy.init(args=args) minimal_client = MinimalClientAsync() minimal_client.send_request() while rclpy.ok(): rclpy.spin_once(minimal_client) if minimal_client.future.done(): try: response = minimal_client.future.result() except Exception as e: minimal_client.get_logger().info( 'Service call failed %r' % (e,)) else: minimal_client.get_logger().info( 'Result of add_three_ints: for %d + %d + %d = %d' % # CHANGE (minimal_client.req.a, minimal_client.req.b, minimal_client.req.c, response.sum)) # CHANGE break minimal_client.destroy_node() rclpy.shutdown() if __name__ == '__main__': main()

To add these features to our package we need to let package.xml know, which for that we need to write:

xml
<exec_depend>tutorial_interfaces</exec_depend>

After making the above edits and saving all the changes, build the package:

Then open two new terminals, source ros2\_ws in each, and run:

bash
ros2 run py_srvcli service

bash
ros2 run py_srvcli client 2 3 1

8.7 Using Parameters in a Class

When making our own nodes we will sometimes need to add parameters that can be set from the launch file.

This tutorial will show we how to create those parameters in a Python class, and how to set them in a launch file.

Creating a Package Open a new terminal and source our ROS installation so that ros2 commands will work.

Follow these instructions to create a new workspace named ros2\_ws.

Recall that packages should be created in the src directory, not the root of the workspace. Navigate into ros2_ws/src and create a new package:

bash
ros2 pkg create --build-type ament_python \ --license Apache-2.0 python_parameters \ --dependencies rclpy

Our terminal will return a message verifying the creation of our package python_parameters and all its necessary files and folders.

The --dependencies argument will automatically add the necessary dependency lines to package.xml and CMakeLists.txt.

Updating the Package Manifesto Because we used the --dependencies option during package creation, we don’t have to manually add dependencies to package.xml or CMakeLists.txt.

As always, though, make sure to add the description, maintainer email and name, and license information to package.xml.

xml
<description>Python parameter tutorial</description> <maintainer email="you@email.com">Your Name</maintainer> <license>Apache License 2.0</license>

Writing our Python Code Inside the ros2_ws/src/python_parameters/python_parameters directory, create a new file called python_parameters_node.py and paste the following code within:

python
import rclpy import rclpy.node class MinimalParam(rclpy.node.Node): def __init__(self): super().__init__('minimal_param_node') self.declare_parameter('my_parameter', 'world') self.timer = self.create_timer(1, self.timer_callback) def timer_callback(self): my_param = self.get_parameter('my_parameter').get_parameter_value().string_value self.get_logger().info('Hello %s!' % my_param) my_new_param = rclpy.parameter.Parameter( 'my_parameter', rclpy.Parameter.Type.STRING, 'world' ) all_new_parameters = [my_new_param] self.set_parameters(all_new_parameters) def main(): rclpy.init() node = MinimalParam() rclpy.spin(node) if __name__ == '__main__': main()

Code Demystified The import statements at the top are used to import the package dependencies.

The next piece of code creates the class and the constructor. The line;

self.declare_parameter('my_parameter', 'world')

of the constructor creates a parameter with the name my_parameter and a default value of world. The parameter type is inferred from the default value, so in this case it would be set to a string type. Next the timer is initialized with a period of 1, which causes the timer_callback function to be executed once a second.

python
class MinimalParam(rclpy.node.Node): def __init__(self): super().__init__('minimal_param_node') self.declare_parameter('my_parameter', 'world') self.timer = self.create_timer(1, self.timer_callback)

The first line of our timer_callback function gets the parameter my_parameter from the node, and stores it in my_param. Next the get_logger function ensures the event is logged. The set_parameters function then sets the parameter my_parameter back to the default string value world. In the case that the user changed the parameter externally, this ensures it is always reset back to the original.

python
def timer_callback(self): my_param = self.get_parameter('my_parameter').get_parameter_value().string_value self.get_logger().info('Hello %s!' % my_param) my_new_param = rclpy.parameter.Parameter( 'my_parameter', rclpy.Parameter.Type.STRING, 'world' ) all_new_parameters = [my_new_param] self.set_parameters(all_new_parameters)

Following the timer_callback is our main. Here ROS is initialized, an instance of the MinimalParam class is constructed, and rclpy.spin starts processing data from the node.

python
def main(): rclpy.init() node = MinimalParam() rclpy.spin(node) if __name__ == '__main__': main()

(Optional) Adding a Parameter Descriptor Optionally, we can set a descriptor for the parameter. Descriptors allow we to specify a text description of the parameter and its constraints, like making it read-only, specifying a range, etc. For that to work, the __init__ code has to be changed to:

python
# ... class MinimalParam(rclpy.node.Node): def __init__(self): super().__init__('minimal_param_node') from rcl_interfaces.msg import ParameterDescriptor my_parameter_descriptor = ParameterDescriptor(description='This parameter is mine!') self.declare_parameter('my_parameter', 'world', my_parameter_descriptor) self.timer = self.create_timer(1, self.timer_callback)

The rest of the code remains the same. Once we run the node, we can then run ros2 param describe /minimal_param_node my_parameter to see the type and description.

Adding an Entry Point Open the setup.py file. Again, match the maintainer, maintainer_email, description and license fields to our package.xml:

python
maintainer='YourName', maintainer_email='you@email.com', description='Python parameter tutorial', license='Apache License 2.0',

Add the following line within the console_scripts brackets of the entry_points field:

python
entry_points={ 'console_scripts': [ 'minimal_param_node = python_parameters.python_parameters_node:main', ], },

Building and Running the Code It’s good practice to run rosdep in the root of our workspace (ros2\_ws) to check for missing dependencies before building:

bash
rosdep install -i --from-path src --rosdistro humble -y

Navigate back to the root of our workspace, ros2\_ws, and build our new package:

bash
colcon build --packages-select python_parameters

Open a new terminal, navigate to ros2\_ws, and source the setup files:

bash
source install/setup.bash

Now run the node. The terminal should return Hello world! every second:

bash
ros2 run python_parameters minimal_param_node
text
[INFO] [parameter_node]: Hello world!

Now we can see the default value of our parameter, but we want to be able to set it ourself. There are two ways to accomplish this.

Changing Output using the Console Make sure the node is running:

bash
ros2 run python_parameters minimal_param_node

Open another terminal, source the setup files from inside ros2\_ws again, and enter the following line:

bash
ros2 param list

There we will see the custom parameter my_parameter. To change it, simply run the following line in the console:

bash
ros2 param set /minimal_param_node my_parameter earth

We know it went well if we get the output Set parameter successful. If we look at the other terminal, we should see the output change to [INFO] [minimal_param_node]: Hello earth!

Since the node afterwards set the parameter back to world, further outputs show:

[INFO] [minimal_param_node]: Hello world!

Changing using the Launch File We can also set parameters in a launch file, but first we will need to add a launch directory. Inside the ros2_ws/src/python_parameters/ directory, create a new directory called launch. In there, create a new file called python_parameters_launch.py

python
from launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): return LaunchDescription([ Node( package='python_parameters', executable='minimal_param_node', name='custom_minimal_param_node', output='screen', emulate_tty=True, parameters=[ {'my_parameter': 'earth'} ] ) ])

Here we can see that we set my_parameter to earth when we launch our node parameter_node. By adding the two lines below, we ensure our output is printed in our console.

bash
output="screen", emulate_tty=True,

Now open the setup.py file. Add the import statements to the top of the file, and the other new statement to the data_files parameter to include all launch files:

bash
import os from glob import glob # ... setup( # ... data_files=[ # ... (os.path.join('share', package_name, 'launch'), glob('launch/*')), ] )

Open a console and navigate to the root of our workspace, ros2\_ws, and build our new package:

bash
colcon build --packages-select python_parameters

Then source the setup files in a new terminal:

bash
source install/setup.bash

Now run the node using the launch file we have just created. The terminal should return the following message the first time:

bash
ros2 launch python_parameters python_parameters_launch.py
text
[INFO] [custom_minimal_param_node]: Hello earth!

Further outputs should show [INFO] [minimal_param_node]: Hello world! every second.

8.8 Managing Dependencies

8.8.1 Explaining Rosdep

rosdep is a dependency management utility that can work with packages and external libraries. It is a command-line utility for identifying and installing dependencies to build or install a package. rosdep is not a package manager in its own right; it is a meta-package manager that uses its own knowledge of the system and the dependencies to find the appropriate package to install on a particular platform. The actual installation is done using the system package manager (e.g. apt on Debian/Ubuntu, dnf on Fedora/RHEL, etc).

It is most often invoked before building a workspace, where it is used to install the dependencies of the packages within that workspace.

It has the ability to work over a single package or over a directory of packages (e.g. workspace).

While the name suggests it is for ROS, rosdep is semi-agnostic to ROS. We can utilize this powerful tool in non-ROS software projects by installing it as a standalone Python package. Successfully running rosdep relies on rosdep keys to be available, which can be downloaded from a public git repository with a few simple commands.

8.8.2 Explaining Pacakge Manifesto

The package.xml is the file in our software where rosdep finds the set of dependencies. It is important that the list of dependencies in the package.xml is complete and correct, which allows all of the tooling to determine the packages dependencies. Missing or incorrect dependencies can lead to users not being able to use our package, to packages in a workspace being built out-of-order, and to packages not being able to be released.

The dependencies in the package.xml file are generally referred to as “rosdep keys”. These dependencies are manually populated in the package.xml file by the package’s creators and should be an exhaustive list of any non-builtin libraries and packages it requires.

These are represented in the following tags (see REP-149 for the full specification):

These are dependencies that should be provided at both build time and run time for our package. For C++ packages, if in doubt, use this tag. Pure Python packages generally don’t have a build phase, so should never use this and should use <exec_depend> instead.

If we only use a particular dependency for building our package, and not at execution time, we can use the <build_depend> tag.

With this type of dependency, an installed binary of our package does not require that particular package to be installed.

However, that can create a problem if our package exports a header that includes a header from this dependency. In that case we also need a <build_export_depend>.

8.9 Creating an Action

We learned about actions previously in the Understanding actions tutorial. Like the other communication types and their respective interfaces (topics/msg and services/srv), we can also custom-define actions in our packages. This tutorial shows you how to define and build an action that we can use with the action server and action client we will write in the next tutorial.

Before we start, let’s setup everything and make sure the requisites are in order:

bash
mkdir -p ros2_ws/src # you can reuse an existing workspace cd ros2_ws/src ros2 pkg create action_tutorials_interfaces

Defining an Action Actions are defined in .action files of the form:

text
# Request --- # Result --- # Feedback

An action definition is made up of three message definitions separated by ---.

A request message is sent from an action client to an action server initiating a new goal.

A result message is sent from an action server to an action client when a goal is done.

Feedback messages are periodically sent from an action server to an action client with updates about a goal.

An instance of an action is typically referred to as a goal.

Say we want to define a new action “Fibonacci” for computing the Fibonacci sequence.

Create an action directory in our ROS package action_tutorials_interfaces :

bash
cd action_tutorials_interfaces mkdir action

Within the action directory, create a file called Fibonacci.action with the following contents:

text
int32 order --- int32[] sequence --- int32[] partial_sequence

The goal request is the order of the Fibonacci sequence we want to compute, the result is the final sequence, and the feedback is the partial_sequence computed so far.

Building an Action Before we can use the new Fibonacci action type in our code, we must pass the definition to the rosidl code generation pipeline.

This is accomplished by adding the following lines to our CMakeLists.txt before the ament_package() line, in the action_tutorials_interfaces :

cmake
find_package(rosidl_default_generators REQUIRED) rosidl_generate_interfaces(${PROJECT_NAME} "action/Fibonacci.action" )

We should also add the required dependencies to our package.xml:

xml
<buildtool_depend>rosidl_default_generators</buildtool_depend> <depend>action_msgs</depend> <member_of_group>rosidl_interface_packages</member_of_group>

Note, we need to depend on action_msgs since action definitions include additional metadata (e.g. goal IDs).

We should now be able to build the package containing the Fibonacci action definition:

bash
cd ~/ros2_ws # Change to the root of the workspace colcon build # Build

We’re done!

By convention, action types will be prefixed by their package name and the word action. So when we want to refer to our new action, it will have the full name action_tutorials_interfaces/action/Fibonacci.

We can check that our action built successfully with the command line tool:

bash
. install/setup.bash # Source our workspace. # Check that our action definition exists ros2 interface show action_tutorials_interfaces/action/Fibonacci

We should see the Fibonacci action definition printed to the screen.

8.10 Writing an Action Server and Client

Actions are a form of asynchronous communication in ROS . Action clients send goal requests to action servers. Action servers send goal feedback and results to action clients.

Writing an Action Server Let’s focus on writing an action server that computes the Fibonacci sequence using the action we created in the Creating an action tutorial.

Until now, we’ve created packages and used ros2 run to run our nodes. To keep things simple in this tutorial, however, we’ll scope the action server to a single file. If we’d like to see what a complete package for the actions tutorials looks like, check out action_tutorials.

Open a new file in our home directory, let’s call it fibonacci_action_server.py, and add the following code:

python
import rclpy from rclpy.action import ActionServer from rclpy.node import Node from action_tutorials_interfaces.action import Fibonacci class FibonacciActionServer(Node): def __init__(self): super().__init__('fibonacci_action_server') self._action_server = ActionServer( self, Fibonacci, 'fibonacci', self.execute_callback) def execute_callback(self, goal_handle): self.get_logger().info('Executing goal...') result = Fibonacci.Result() return result def main(args=None): rclpy.init(args=args) fibonacci_action_server = FibonacciActionServer() rclpy.spin(fibonacci_action_server) if __name__ == '__main__': main()

8.11 Writing a Launch File

The launch system in ROS is responsible for helping the user describe the configuration of their system and then execute it as described . The configuration of the system includes:

  • what programs to run, where to run them,

  • what arguments to pass them, and

  • ROS-specific conventions which make it easy to reuse components throughout the system by giving them each a different configuration

It is also responsible for monitoring the state of the processes launched, and reporting and/or reacting to changes in the state of those processes.

Launch files written in XML , YAML , or Python can start and stop different nodes as well as trigger and act on various events.

The package providing this framework is launch_ros, which uses the non-ROS-specific launch framework underneath.

So now we got the preliminary information out of the way, let us start with working on writing our own launch file.

Creating a Launch Directory We start by creating a new directory to store our launch files:

bash
mkdir launch

Writing our Launch File Let’s put together a ROS launch file using the turtlesim package and its executables. As mentioned previously, this can either be in XML, YAML, or Python. However for the sake of coherence, we shall only look at the Python version of it.

Please copy and paste the code into launch/turtlesim\_mimic\_launch.py file:

python
from launch import LaunchDescription from launch_ros.actions import Node def generate_launch_description(): return LaunchDescription([ Node( package='turtlesim', namespace='turtlesim1', executable='turtlesim_node', name='sim' ), Node( package='turtlesim', namespace='turtlesim2', executable='turtlesim_node', name='sim' ), Node( package='turtlesim', executable='mimic', name='mimic', remappings=[ ('/input/pose', '/turtlesim1/turtle1/pose'), ('/output/cmd_vel', '/turtlesim2/turtle1/cmd_vel'), ] ) ])

Understanding the Launch File All of the launch files above are launching a system of three (3) nodes, all from the turtlesim package. The goal of the system is to launch two (2) turtlesim windows, and have one turtle mimic the movements of the other.

When launching the two (2) turtlesim nodes, the only difference between them is their namespace values. Unique namespaces allow the system to start two (2) nodes without node name or topic name conflicts. Both turtles in this system receive commands over the same topic and publish their pose over the same topic.

With unique namespaces, messages meant for different turtles can be distinguished.

The final node is also from the turtlesim package, but a different executable:

mimic.

This node has added configuration details in the form of remappings. mimic’s input/pose topic is remapped to turtlesim1/turtle1/pose and it’s output/cmd\_vel topic to turtlesim2/turtle1/cmd\_vel.

This means mimic will subscribe to turtlesim1/sim pose topic and republish it for turtlesim2/sim velocity command topic to subscribe to.

In other words, turtlesim2 will mimic turtlesim1’s movements.

Now to see what is going on with the launch file. These import statements pull in some Python launch modules.

python
from launch import LaunchDescription from launch_ros.actions import Node

Next, the launch description itself begins:

python
def generate_launch_description(): return LaunchDescription([ Node( ])

The first two actions in the launch description launch the two turtlesim windows:

python
Node( package='turtlesim', namespace='turtlesim1', executable='turtlesim_node', name='sim' ), Node( package='turtlesim', namespace='turtlesim2', executable='turtlesim_node', name='sim' ),

The final action launches the mimic node with the remaps:

python
Node( package='turtlesim', executable='mimic', name='mimic', remappings=[ ('/input/pose', '/turtlesim1/turtle1/pose'), ('/output/cmd_vel', '/turtlesim2/turtle1/cmd_vel'), ] )

Launching the Package To run the launch file created above, enter into the directory we created earlier and run the following command:

bash
cd launch ros2 launch turtlesim_mimic_launch.py

Two turtlesim windows will open, and we will see the following [INFO] messages telling we which nodes our launch file has started:

text
[INFO] [launch]: Default logging verbosity is set to INFO [INFO] [turtlesim_node-1]: process started with pid [11714] [INFO] [turtlesim_node-2]: process started with pid [11715] [INFO] [mimic-3]: process started with pid [11716]

To see the system in action, open a new terminal and run the ros2 topic pub command on the turtlesim1/turtle1/cmd\_vel topic to get the first turtle moving:

bash
ros2 topic pub -r 1 \ /turtlesim1/turtle1/cmd_vel geometry_msgs/msg/Twist \ "{linear: {x: 2.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: -1.8}}"

We will see both turtles following the same path.

Checking the Graph While the system is still running, open a new terminal and run rqt_graph to get a better idea of the relationship between the nodes in our launch file.

Run the command:

bash
rqt_graph

A hidden node (the ros2 topic pub command we ran) is publishing data to the turtlesim1/turtle1/cmd\_vel topic on the left, which the turtlesim1/sim node is subscribed to. The rest of the graph shows what was described earlier: mimic is subscribed to turtlesim1/sim ’s pose topic, and publishes to turtlesim2/sim ’s velocity command topic.