ROS Package

In this section you will:

  • create a new ros package

  • write a node in python

  • run a node via ros2 run

  • use the tools ros2 node info and ros2 topic echo

Attention

The following tutorial is not meant as a step-by-step solution for the first assignment. These are just toy examples to demonstrate how to use ROS and interact with the simulated BlueROV in an easy to follow manner. Therefore, we do not claim that these code snippets are complete and we use some funny names at times. Please do not copy-paste them.

Hint

The ROS documentation contains detailed tutorials and descriptions if you need them.

Create A Package

If you have completed all the installation instructions in Install ROS, you have already created a Catkin workspace at ~/fav/ros2. The workspace contains several directories:

~/fav/ros2
├── build
├── install
├── log
└── src

Probably the only one you will be working with is the src directory. This is the place where the so called packages are.

For our code directories to be recognized as packages, two files are required:

  • package.xml

  • CMakeLists.txt

Otherwise a package is nothing but a normal directory containing arbitrary files and subdirectories. Packages can also be in arbitrary subdirectories, since the build system will look for directories containing a package.xml recursively to identify packages. An example for this is the fav repository you cloned during the setup instructions. The fav directory itself is not a package, but contains packages as subdirectories like keyboard_control or scenario_msgs.

For this guide it is not necessary to go into details too much. But if you like to know more about packages, you can read the article about packages in the ROS Docs.

Hint

In ROS2, there is the option to create native Python packages. For this class, we stick to the CMake way of organizing packages. Even if we write pure Python packages. So in this regard, our instructions differ from the official documentation.

Go to the src directory

$ cd ~/fav/ros2/src

and create the package directory

$ mkdir awesome_package

Remember, we need at least package.xml and CMakeLists.txt. Almost minimal examples are presented in the following. Take a look at the highlighted lines. Replace the project’s name with your own package name.

CMakeLists.txt
 1cmake_minimum_required(VERSION 3.5)
 2project(awesome_package)
 3find_package(ament_cmake REQUIRED)
 4find_package(ament_cmake_python REQUIRED)
 5find_package(rclpy REQUIRED)
 6
 7install(PROGRAMS
 8  DESTINATION lib/${PROJECT_NAME}
 9)
10
11ament_package()
package.xml
 1<?xml version="1.0"?>
 2<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
 3<package format="3">
 4  <name>awesome_package</name>
 5  <version>0.0.0</version>
 6  <description>Our super awesome package</description>
 7
 8  <maintainer email="someones.mail.address@tuhh.de">Someones name</maintainer>
 9
10  <!-- One license tag required, multiple allowed, one license per tag -->
11  <!-- Commonly used license strings: -->
12  <!--   BSD, MIT, Boost Software License, GPLv2, GPLv3, LGPLv2.1, LGPLv3 -->
13  <license>GPLv2</license>
14
15  <url type="website">hippocampusrobotics.github.io/docs</url>
16
17  <author email="someones.mail@tuhh.de">Someones name</author>
18
19  <buildtool_depend>ament_cmake</buildtool_depend>
20  <buildtool_depend>ament_cmake_python</buildtool_depend>
21
22  <depend>rclpy</depend>
23
24  <!-- The export tag contains other, unspecified, tags -->
25  <export>
26    <build_type>ament_cmake</build_type>
27  </export>
28</package>

That’s it. You have just created your first package. Your package structure should look similar to:

~/fav/ros2/src
└── awesome_package
    ├── CMakeLists.txt
    └── package.xml

We can now build our workspace

$ build_ros

and source the newly created package.

$ . ~/.zshrc

This only needs to be done once a new package is created. Nothing bad happens if we are a bit overly cautious regarding sourcing our .zshrc. But it does nothing good either. So we might want to save it up for the cases where it is actually required.

Note

Remeber to either close and reopen all terminals or source in each terminal individually.

If the commands mentioned above completed without errors, we can check if our newly created is detected correctly. The following command should give as the installation path of our package.

$ ros2 pkg prefix awesome_package

In case things did not work out as expected, we might get Package not found as response. This indicates that we (most likely) messed something up while following the instructions above. Double check everything and if this does not fix the problem ask your favorite research associate.

Write A Node

In general, you have the choice to write nodes either in Python or in C++. For the sake of simplicity we recommend Python. If you haven’t already worked with one of these languages, in some regards Python might feel similiar to Matlab.

Before we can write a node, we create a nodes/ directory to keep things neat and clean. It is not strictly required (ROS will find your node as long as it is in your package, no matter in which subdirectory it is), but it complies with conventions.

Right click awesome_package and choose New Folder and name it nodes. Right click nodes and choose New File. Name it setpoint_publisher.py. It should open automatically.

../_images/vscode_create_node.gif

We have to make the Python file executable. To do so, enter the following command in your terminal (for example the integrated one in VS Code):

$ chmod +x ~/fav/ros2/src/awesome_package/nodes/setpoint_publisher.py

Hint

Just in case the integrated terminal is not open: You can open it with Ctrl + Shift + `.

The general syntax is chmod +x PATH_TO_THE_FILE.

Note

Each time you create a new node, make sure you have made it executable.

The first line of your node needs to be:

1#!/usr/bin/env python3

so your system knows your file should be executed as a Python file.

Your first node could look like:

 1#!/usr/bin/env python3
 2
 3import math
 4
 5import rclpy
 6from hippo_control_msgs.msg import ActuatorSetpoint
 7from rclpy.node import Node
 8
 9
10class MyFirstNode(Node):
11
12    # The __init__ function gets called when we create the object, i.e.
13    # when we run something like:
14    # node = MyFirstNode()
15    def __init__(self):
16        # Nodes need unique names, therefore we initialize the node
17        # with a name not used yet.
18        super().__init__(node_name='my_first_node')
19
20        # Create publishers. We need to specify the message type and
21        # the topic name. The last argument specifies the queue length.
22        self.thrust_pub = self.create_publisher(ActuatorSetpoint,
23                                                'thrust_setpoint', 1)
24        self.torque_pub = self.create_publisher(ActuatorSetpoint,
25                                                'torque_setpoint', 1)
26
27        # Create a timer. We use it to call a function with a defined rate.
28        # In this case we want to publish the setpoints with 50 Hz.
29        # The name of the function we want to call is given as the second
30        # argument.
31        self.timer = self.create_timer(1 / 50, self.on_timer)
32
33    def on_timer(self):
34        self.publish_setpoints()
35
36    def publish_setpoints(self):
37        # create the message object
38        thrust_msg = ActuatorSetpoint()
39
40        # get the current time for the message's timestamp
41        now = self.get_clock().now()
42        thrust_msg.header.stamp = now.to_msg()
43
44        # get the time as floating point number in seconds to use for
45        # calculating thrust and torque values in this toy example
46        t = now.nanoseconds * 1e-9
47
48        # fill the message object's fields
49        thrust_msg.x = 0.5 * math.sin(t)
50        thrust_msg.y = -0.5 * math.sin(t)
51        thrust_msg.z = 0.5 * math.cos(t)
52
53        # repeat for torque setpoint message
54        torque_msg = ActuatorSetpoint()
55        torque_msg.header.stamp = now.to_msg()
56        torque_msg.x = 0.4 * math.sin(t)
57        torque_msg.y = -0.4 * math.sin(t)
58        torque_msg.z = 0.4 * math.cos(t)
59
60        # publish the messages using the publishers we created during
61        # the object initialization
62        self.thrust_pub.publish(thrust_msg)
63        self.torque_pub.publish(torque_msg)
64
65
66def main():
67    rclpy.init()
68    node = MyFirstNode()
69    rclpy.spin(node)
70
71
72if __name__ == '__main__':
73    main()

Run A Node

Attention

For each node we have to modify the CMakeLists.txt of the corresponding package. Add the node’s path relative to the package’s root to the install() call.

For our first node we add the highlighted line to the CMakeLists.txt.

install(PROGRAMS
  nodes/setpoint_publisher.py
  DESTINATION lib/${PROJECT_NAME}
)

Every time you modify the CMakeLists.txt rebuild your workspace with build_ros and to be super save you might also want to resource your workspace setup with . ~/.zshrc. The latter is only required if we added a new node.

If you want to run a Python program, normally you would use a command like python3 /path/to/your/file/python_file.py. This would work for our node, too. But instead of running our node by entering python ~/fav/ros2/src/awesome_package/nodes/setpoint_publisher.py, where we have to explicitly tell Python where it can find our file setpoint_publisher.py, we can use ros2 run. One of the advantages of ros2 run is that we do not have to know where the program/node is that we want to run. The command finds the source file on its own.

The general usage of the ros2 run command is ros2 run <package_name> <executable_name>. So for our awesome_package and its setpoint_publisher.py it would be:

$ ros2 run awesome_package setpoint_publisher.py

If you try to do so right now, you will likely get an error message No executable found.

We created a package, but we haven’t built our workspace since we modified CMakeLists.txt (remember, that we are supposed to rebuild our workspace each time we modify this file?).

$ build_ros

Note

Every time we create a new package, or create a new node in an existing package, we need to build our workspace with build_ros and apply the updated package paths with . ~/.zshrc.

Now, we should be ready to finally run our code

$ ros2 run awesome_package setpoint_publisher.py

Hint

You can use Tab to use the shell’s ability to auto-complete your commands. If the auto-completion is unambigous, a single hit will suffice. If there is more than one auto-complete option, hit Tab twice to show the different options.

Hint

Just to remind you: you stop running programs in a terminal by the shortcut Ctrl + C.

In the node’s source code you can see that the sent setpoints are sin and cos signals.

We have started the setpoint_publisher.py node. But since it just publishes ROS messages we can’t see any output in the terminals. We can use command line tools ros2 node and ros2 topic to get some insights on what is going on in the background hidden from our curious eyes. With ros2 node info /name/of/our/node we can get various information on our node. For example what publications and what subscriptions it has. Or in other words: what are the topics the node wants to receive data on and what are the topics it ouputs data on.

To get a list of all nodes, we run

$ ros2 node list

which in our case should yield:

/my_first_node

To get more information on this node, we run

$ ros2 node info /my_first_node

which in turn yields:

 /my_first_node
Subscribers:

Publishers:
  /parameter_events: rcl_interfaces/msg/ParameterEvent
  /rosout: rcl_interfaces/msg/Log
  /thrust_setpoint: hippo_control_msgs/msg/ActuatorSetpoint
  /torque_setpoint: hippo_control_msgs/msg/ActuatorSetpoint
Service Servers:
  /my_first_node/describe_parameters: rcl_interfaces/srv/DescribeParameters
  /my_first_node/get_parameter_types: rcl_interfaces/srv/GetParameterTypes
  /my_first_node/get_parameters: rcl_interfaces/srv/GetParameters
  /my_first_node/get_type_description: type_description_interfaces/srv/GetTypeDescription
  /my_first_node/list_parameters: rcl_interfaces/srv/ListParameters
  /my_first_node/set_parameters: rcl_interfaces/srv/SetParameters
  /my_first_node/set_parameters_atomically: rcl_interfaces/srv/SetParametersAtomically
Service Clients:

Action Servers:

Action Clients:

Hint

Again, we can use Tab to auto-complete the node name after we have started writing the first few characters. Start using this feature if you haven’t already!

The first two publishers are internally created by ROS2. We do not care about them for now. The other publishers are the ones we have created with the program that we have written.

To see what messages the node is actually publishing, we could use ros2 topic echo /the/topic/name/to/echo.

Note

We ad --once at the end of the command to echo only a single message. If you omit this argument, ros2 topic echo will continue to print messages until you stop it with Ctrl + C.

These two commands are great to get at least some insights on what is going on during the execution of our node. But most of us will find it rather cumbersome to evaluate the echoed data in realtime. I mean, would you claim to be able to see that the echoed data is actually the output of a sine function? So, some proper plotting tool might come in handy here.

We can use plotjuggler to visualize the data. General information to plotjuggler can be found on the GitHub Page and some step-by-step instructions in the section Real-Time Plotting.