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:
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
src
subdirectory.
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:
At
this
moment
if
we
were
to ls
into
our
directory
we
will
only
see
one
src
,
which
is
to
be
expected
as
it
is
created
just
a
second
ago.
Now let’s populate our newly created environment with some tutorial files from the official ROS repo:
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.
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
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:
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.
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
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
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.
Depending on how we installed ROS , either from source or binaries, and which platform we’re on, our exact source command will vary:
Creating a New Directory
Best practice is to create a
ros2\_ws
, for “development workspace”:
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:
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
).
If we already have all our dependencies, the console will return:
Packages
declare
their
dependencies
in
the package.xml
file.
Building
the
Workspace
From
the
root
of
our
workspace (ros2\_ws
),
we
can
now
build
our
packages
using
the
command:
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:
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:
Time to go into the root of our workspace:
In the root, source our overlay:
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:
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
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.
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:
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
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:
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
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:
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
rclpy
is
the
ROS
library
written
for
Python.
The next statement imports the built-in string message type which the node uses to structure the data it passes on the topic.
These
aforementioned
lines
represent
the
node’s
package.xml
,
which
we
will
have
a
look
at
in
just
a
little
bit.
Next,
the MinimalPublisher
class
is
created,
which
inherits
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,
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.
timer_callback
creates
a
message
with
the
counter
value
appended,
and
publishes
it
to
the
console
with get_logger().info
.
Lastly, the main function is defined.
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.
As
mentioned
previously,
make
sure
to
fill
in
the <description>
, <maintainer>
and <license>
tags:
After
the
lines
above,
add
the
following
dependencies
corresponding
to
our
node’s
import
statements:
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
:
Add
the
following
line
within
the console_scripts
brackets
of
the entry_points
field:
Checking
the
Configuration
File
The
contents
of
the setup.cfg
file
should
be
correctly
populated
automatically,
like
so:
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:
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.
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.
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
The main definition is almost exactly the same, replacing the creation and spinning of the publisher with the 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:
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
.srv
file.
The example used here is a simple integer addition system;
one
node
requests
the
sum
of
two
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:
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:
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.
Updating
the
Configuration
Add
the
same
information
to
the setup.py
file
for
the maintainer
, maintainer_email
, description
and
license
fields:
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:
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.
The MinimalService
class
constructor
initializes
the
node
with
the
name minimal_service
.
Then,
it
creates
a
service
and
defines
the
type,
name,
and
callback.
The definition of the service callback receives the request data, sums it, and returns the sum as a 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
.
[
/]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:
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:
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.
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.
Below
the
constructor
is
the send_request
method,
which
will
send
the
request
and
spin
until
it
receives
the
response
or
fails.
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_points
field
of
our setup.py
file
should
look
like
this:
It’s
good
practice
to
run rosdep
in
the
root
of
our
workspace (ros2\_ws
)
to
check
for
missing
dependencies
before
building:
Navigate
back
to
the
root
of
our
workspace, ros2\_ws
,
and
build
our
new
package:
Open
a
new
terminal,
navigate
to ros2\_ws
,
and
source
the
setup
files:
Now run the service node:
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:
Return to the terminal where our service node is running. We will see that it published log messages when it received the request:
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:
tutorial_interfaces
is
the
ament_cmake
package,
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:
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:
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:
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:
This is our custom service which requests three a
, b
, and c
, and responds with an integer called sum
.
CMakeLists
To
convert
the
interfaces
we
defined
into
language-specific
code
CMakeLists.txt
:
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
:
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:
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:
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:
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
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
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.
Once we have done the necessary changes let’s build our package and execute it.
Then open two new terminals, source ros2\_ws
in each, and run:
Since Num.msg
relays only an integer, the talker should only be publishing integer values, as opposed to the string it published previously:
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
Service
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
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:
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:
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:
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.
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:
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.
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.
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.
(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:
# ... 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:
Add the following line within the console_scripts
brackets of the entry_points
field:
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:
Navigate back to the root of our workspace, ros2\_ws
, and build our new package:
Open a new terminal, navigate to ros2\_ws
, and source the setup files:
Now run the node. The terminal should return Hello world! every second:
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:
Open another terminal, source the setup files from inside ros2\_ws
again, and enter the following line:
There we will see the custom parameter my_parameter
. To change it, simply run the following line in the console:
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
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.
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:
Open a console and navigate to the root of our workspace, ros2\_ws
, and build our new package:
Then source the setup files in a new terminal:
Now run the node using the launch file we have just created. The terminal should return the following message the first time:
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:
Defining an Action
Actions are defined in .action
files of the form:
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
:
Within the action directory, create a file called Fibonacci.action with the following contents:
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
:
We should also add the required dependencies to our package.xml:
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:
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:
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:
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
-
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
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:
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:
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
When launching the two
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.
Next, the launch description itself begins:
The first two actions in the launch description launch the two turtlesim windows:
The final action launches the mimic node with the remaps:
Launching the Package To run the launch file created above, enter into the directory we created earlier and run the following command:
Two turtlesim windows will open, and we will see the following [INFO] messages telling we which nodes our launch file has started:
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:
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:
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.