Jeremie Deray
on 7 August 2019
Please note that this blog post has old information that may no longer be correct. We invite you to read the content as a starting point but please search for more updated information in the ROS documentation.
Following our previous post on ROS 2 CLI (Command Line Interface), we will see here how one can extend the set of existing CLI tools by introducing a new command and its related verb(s).
As support for this tutorial, we will create a ‘Hello World’ example so that the new command will be hello
and the new verb will be world
. The material is readily available on github.
Compared to ROS 1, the ROS 2 CLI has been entirely re-designed in Python offering a clean API, a single entry-point (the keyword ros2
) and more importantly for the topic at hand, a plugin-like interface using Python entry points.
This new interface allows one to easily extend the existing set of commands and verbs using a few boiler-plate classes and the actual implementation of our new tools.
Let’s get to it!
Setting up the package
First we will create a new ROS 2 python package and the necessary sub-folders:
$ cd ~/ros2_ws/src
$ mkdir ros2hellocli && cd ros2hellocli
$ mkdir command verb
While the ros2hellocli
will be the root folder for this project, the command
folder will contain a command extension point that allows ROS 2 CLI to discover the new command. Similarly, the verb
folder will contain the verb extension point(s) which will hold the actual implementation of our new functionality.
But first, let us not forget to turn those sub-folders into Python packages:
$ touch command/__init__.py verb/__init__.py
Now that we have our project structure ready, we will set up the boiler-plate code mentioned earlier, starting with the classical package manifest and setup.py
files.
$ touch package.xml
And copy the following,
<?xml version="1.0"?>
<package format="2">
<name>ros2hellocli</name>
<version>0.0.0</version>
<description>
The ROS 2 command line tools example.
</description>
<maintainer email="jeremie.deray@example.org">Jeremie Deray</maintainer>
<license>Apache License 2.0</license>
<depend>ros2cli</depend>
<export>
<build_type>ament_python</build_type>
</export>
</package>
Then,
$ touch setup.py
And copy the following,
from setuptools import find_packages
from setuptools import setup
setup(
name='ros2hellocli',
version='0.0.0',
packages=find_packages(exclude=['test']),
install_requires=['ros2cli'],
zip_safe=True,
author='Jeremie Deray',
author_email='jeremie.deray@example.org',
maintainer='Jeremie Deray',
maintainer_email='jeremie.deray@example.org',
url='https://github.com/artivis/ros2hellocli',
download_url='https://github.com/artivis/ros2hellocli/releases',
keywords=[],
classifiers=[
'Environment :: Console',
'Intended Audience :: Developers',
'License :: OSI Approved :: Apache Software License',
'Programming Language :: Python',
],
description='A minimal plugin example for ROS 2 command line tools.',
long_description="""The package provides the hello command as a plugin example of ROS 2 command line tools.""",
license='Apache License, Version 2.0',
)
As those two files are fairly common in the ROS world, we skip detailing them and refer the reader to ROS documentation for further explanations (package manifest on ROS wiki).
When creating a new CLI tool, remember however to edit the appropriate entries such as name
/authors
/maintainer
etc.
We will also notice that the package depend upon ros2cli
since it is meant to extend it.
Creating a ROS 2 CLI command
Now we shall create the new command hello
and its command entry-point. First we will create a hello.py
file in the command
folder,
$ touch command/hello.py
and populate it as follows,
from ros2cli.command import add_subparsers
from ros2cli.command import CommandExtension
from ros2cli.verb import get_verb_extensions
class HelloCommand(CommandExtension):
"""The 'hello' command extension."""
def add_arguments(self, parser, cli_name):
self._subparser = parser
verb_extensions = get_verb_extensions('ros2hellocli.verb')
add_subparsers(
parser, cli_name, '_verb', verb_extensions, required=False)
def main(self, *, parser, args):
if not hasattr(args, '_verb'):
self._subparser.print_help()
return 0
extension = getattr(args, '_verb')
return extension.main(args=args)
The content of hello.py
is fairly similar to any other command entry-point.
With the new command being defined, we will now edit the setup.py
file to advertise this new entry-point so that the CLI framework can find it. Within the setup()
function in setup.py
, we append the following lines:
...
tests_require=['pytest'],
entry_points={
'ros2cli.command': [
'hello = ros2hellocli.command.hello:HelloCommand',
]
}
)
From now on, ROS 2 CLI framework should be able to find the hello
verb,
$ cd ~/ros2_ws
$ colcon build --symlink-install --packages-select ros2hellocli
$ source install/local_setup.bash
$ ros2 [tab][tab]
Hitting the [tab]
key will trigger the CLI auto-completion which will display in the terminal the different options available. Among those options should appear hello
,
$ ros2 [tab][tab]
action extensions msg run topic
bag ------> hello <------ multicast security
component interface node service
daemon launch param srv
extension_points lifecycle pkg test
Since the CLI framework can find hello
, we should also be able to call it,
$ ros2 hello
usage: ros2 hello [-h]
Call `ros2 hello <command> -h` for more detailed usage. ...
The 'hello' command extension
optional arguments:
-h, --help show this help message and exit
Commands:
Call `ros2 hello <command> -h` for more detailed usage.
It works!
Fairly simple so far isn’t it? Notice that the output shown in the terminal is the same as calling ros2 hello --help
.
Creating a ROS 2 CLI word
Alright, so now that we have successfully created the new command hello
, we will now create its associated new verb world
. Notice that the following is transposable to virtually any command.
Just like commands, verbs rely on the same ‘entry-point’ mechanism, we therefore create a world.py
file in the verb
folder,
$ cd ~/ros2_ws/src/ros2hellocli
$ touch verb/world.py
and populate it as follows,
from ros2cli.verb import VerbExtension
class WorldVerb(VerbExtension):
"""Prints Hello World on the terminal."""
def main(self, *, args):
print('Hello, ROS 2 World!')
As previously, we have to advertise this new entry-point in setup()
as well by appending the following,
...
tests_require=['pytest'],
entry_points={
'ros2cli.command': [
'hello = ros2hellocli.command.hello:HelloCommand',
],
'ros2hellocli.verb': [
'world = ros2hellocli.verb.world:WorldVerb',
]
}
)
The ROS 2 CLI framework should be able to find the world
command,
$ cd ~/ros2_ws
$ colcon build --symlink-install --packages-select ros2hellocli
$ source install/local_setup.bash
$ ros2 hello [tab][tab]
world
and we should be able to call it,
$ ros2 hello world
Hello, ROS 2 World!
Et voilà! We successfully created a CLI command/verb duo.
Although this example is working fine, we will improve it a little in order to cover two more aspects of the CLI framework, the first one being handling user input arguments and the second being related to good practice.
We will start by creating an api
Python package:
$ cd ~/ros2_ws/src/ros2hellocli
$ mkdir api
$ touch api/__init__.py
It will contain all of the factorized code, everything that one can turn into small and useful Python functions/classes for re-use and prevent code duplication. And that is precisely what we will do with our print
call. In the api/__init__.py
file, we will define the following functions,
def get_hello_world():
return 'Hello, ROS 2 World!'
def get_hello_world_leet():
return 'He110, R0S 2 W04ld!'
From there we will modify the WorldVerb
class so that is calls one of the above function by default and the other if the user passes a given flag option to the CLI. To do so we modify the verb/world.py
file as follows,
from ros2cli.verb import VerbExtension
from ros2hellocli.api import get_hello_world, get_hello_world_leet
class WorldVerb(VerbExtension):
"""Prints Hello World on the terminal."""
def add_arguments(self, parser, cli_name):
parser.add_argument(
'--leet', '-l', action='store_true',
help="Display the message in 'l33t' form.")
def main(self, *, args):
message = get_hello_world() if not args.leet else get_hello_world_leet()
print(message)
Let us test this new option,
$ cd ~/ros2_ws
$ colcon build --symlink-install --packages-select ros2hellocli
$ ros2 hello world
Hello, ROS 2 World!
$ ros2 hello world --leet
He110, R0S 2 W04ld!
Isn’t it great?
So now that we have now covered the basics of adding both a new ROS 2 CLI command and verb, how would you expand the hello
command with a new universe
verb?
Notice that you may find many examples in the ros2cli github repository to help you creating powerful CLI tools. If you are not yet familiar with all existing tools, you can have a look at the ROS 2 CLI cheats sheet we put together to help you get up to date.
Come join the discussion and tell us what new CLI tool you have developed!