Skip to main content
Version: 2024

Introduction to ROS2

Robot Operating System 2 aka ROS2 is a set of open-source libraries and tools used to build robot applications.

The ROS2 Graph

The ROS2 Graph describes the general terminology and structure of ROS2 at the user-level. The ROS2 graph consists of:

  • Nodes
  • Topics
  • Messages
  • Parameters
  • Services
  • Actions

Nodes

Nodes are instances of executables that act as individual computation entitites in the ROS2 environment. They perform specific tasks, such as processing sensor data or controlling hardware. Nodes communicate with each other by publishing and subscribing to topics or by using services. Nodes typically consist of combinations of publishers, subscribers, and services. Nodes may also communicate using services, actions, and parameters.

Topics

Topics are named buses that nodes use to exchange messages. Nodes can both publish messages on a topic and receive messages in a callback function on a subscribed topic. Each topic is limited to an individual message type specified in a node's executable.

Messages

Messages are data structures used to communicate information betyouen nodes via topics. These structures contain combinations of fields and data types, and are defined in .msg files. The field is the name of specific data in the structure, while the data type is typically either a primitive data type or another message type. Messages define specific information such as sensor data or control input.

Parameters

Parmaeters are configuration values assigned to nodes. Parameters can either be node-specific or applied to multiple nodes in the ROS2 environment.

Services

Services provide an alternate communication method with a request-response mechanism. A client sends requests to a server and receives responses after the sevice is complete.

Actions

Actions are another communication alternative intended for long-running tasks. Actions consist of three elements: a goal, status updates, and a result.

Creating a ROS2 Workspace

For ROS2 packages to be accessible to the system, the ROS2 installation and workspaces must be source. The ROS2 installation contains all default packages and supporting software. ROS2 workspaces are user-defined locations that contain packages, nodes, etc.

Configure the .zshrc file to source the ROS2 installation. In a terminal on the backseat, enter the following command:

echo "source /opt/ros/jazzy/setup.zsh" >> ~/.zshrc

Before, the .zshrc file was edited by opening the file and then copying/pasting new lines. The above command adds source /opt/ros/jazzy/setup.zsh as a new line at the end of the .zshrc file. Resource the .zshrc file to apply the changes:

source ~/.zshrc

Now, create a ROS2 workspace for this course:

cd && mkdir auvc_ws && cd auvc_ws && mkdir src

This ROS2 workspace is called auvc_ws and is located at the home directory. It contains an empty src directory for initialization. In the future, all your custom packages must be located in src.

Compile the empty workspace:

cd ~/auvc_ws && colcon build

The ROS2 workspace now contains directories named build, install, logs, src.

The build directory is used during compiling to build executables from packages in src. The install directory contains the final results from build. The logs directory contains log data about the workspace.

Configure the .zshrc file to source the auvc_ws workspace:

echo "source $HOME/auvc_ws/install/setup.zsh" >> ~/.zshrc

Resource the .zshrc file:

source ~/.zshrc

Now all installation files and packages within your auvc_ws workspace will be sourced in each new terminal. When you compile new packages or recompile existing packages, you should resource the workspace with source ~/auvc_ws/install/setup.zsh.

Creating ROS2 Packages

Create a new ROS2 package called intro_to_ros for this lesson. First, navigate to ~/auvc_ws/src then use ros2 pkg create:

cd ~/auvc_ws/src
ros2 pkg create --build-type ament_python intro_to_ros

Now, open the ROS2 package in Code:

code ~/auvc_ws/src/intro_to_ros

Since you created the package on the backseat, it is not currently backed up on GitHub. Go to https://github.com on your laptop and create an empty repository called intro_to_ros. This repository will store all files for your intro_to_ros package. Now sync your package on backseat to your Git repository.

Now, on the backseat, initialize the Git repository:

cd ~/auvc_ws/src/intro_to_ros
git init

Add the remote repository:

git remote add origin https://github.com/YOUR_GITHUB_USERNAME/intro_to_ros.git

Add your files and commit:

git add .
git commit -m "initial commit"
git branch -M main

Now, to publish your branch and push your initial commit, go to the Source Control tab and click Publish Branch.

Publisher/Subscriber Example

This section contains a ROS2 tutorial for writing publisher and subscriber nodes that communicate.

Each of these nodes has a specific purpose: the publisher node publishes Vector3 message with random values at a frequency of 1 Hz, while the subscriber node receives those Vector3 message and prints the corresponding unit vector. This example differs from one in the presentation and is a better example of inter-node communication using topics and messages.

Publisher Node

Create a new file in intro_to_ros/intro_to_ros/ called publisher.py.

Add the shebang to the first line of the file:

#!/usr/bin/env python3

Next, specify the imports. In all nodes, import rclpy and from rclpy.node import Node are required to properly use the ROS2 Python3 client library. For this specific node, since you're using Vector3 from the geometry_msgs package, you must import it as well. Other libraries like numpy or opencv2 can be imported as needed.

import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Vector3

import random

Now, create a class for the node which inherits from Node:

class TutorialPublisher(Node):

Within the class's constructor, you start the node and define attributes relating to ROS2 communication.

    def __init__(self):
super().__init__("tutorial_publisher")
self.publisher = self.create_publisher(
Vector3,
"/tutorial/vector3",
10
)
self.publisher_timer = self.create_timer(
1.0, self.run_node
)
self.get_logger().info("starting publisher node")

This constuctor does four things:

  1. Start a node named tutorial_publisher in an instance of the TutorialPublisher class.
  2. Create a publisher that broadcasts Vector3 messages to the topic named /tutorial/vector3.
  3. Create a timer that runs the run_node method every 1.0 seconds.
  4. Log that the node is starting in the terminal.

For this tutorial, the run_node method will publish random Vector3 messages.

    def run_node(self):
msg = Vector3()
msg.x = random.uniform(-10.0, 10.0)
msg.y = random.uniform(-10.0, 10.0)
msg.z = random.uniform(-10.0, 10.0)
self.publisher.publish(msg)
self.get_logger().info(f"Vector3\n\tx: {msg.x}\ty: {msg.y}\tz: {msg.z}")

This method, when called, will generate a Vector3 message with pseudo random values.

Lastly, define the main function. rclpy.init starts the ROS2 Python3 client, node creates an instance of the TutorialPublisher class, rclpy.spin keeps node running until there is an exception, destroy_node destroys node at the end of the program's lifespan, and rclpy.shutdown closes the ROS2 Python3 client if it is still active.

def main(args=None):
rclpy.init(args=args)
node = TutorialPublisher()

try:
rclpy.spin(node)
except KeyboardInterrupt:
print("\nKeyboardInterrupt received, shutting down...")
finally:
node.destroy_node()
if rclpy.ok():
rclpy.shutdown()

if __name__=="__main__":
main()

Subscriber Node

Create a new file in intro_to_ros/intro_to_ros/ called subscriber.py.

Add the shebang and imports:

#!/usr/bin/env python3

import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Vector3

import numpy as np

Now, create a class for the node which inherits from Node:

class TutorialSubscriber(Node):

Within the class's constructor, you start the node and define attributes relating to ROS2 communication.

    def __init__(self):
super().__init__("tutorial_subscriber")
self.subscriber = self.create_subscription(
Vector3,
"/tutorial/vector3",
self.callback,
10
)
self.subscriber
self.get_logger().info("starting subscriber node")

This constuctor does three things:

  1. Start a node named tutorial_subscriber in an instance of the TutorialPublisher class.
  2. Create a subscriber that receives Vector3 messages on the topic named /tutorial/vector3 in the method self.callback.
  3. Log that the node is starting in the terminal.

For each subscriber that a node contains, there must be a callback method. Define a callback method for the subscriber attribute called subscription:

    def callback(self, msg):
magnitude = np.sqrt(msg.x ** 2 + msg.y ** 2 + msg.z ** 2)
data = Vector3()
data.x = msg.x / magnitude
data.y = msg.y / magnitude
data.z = msg.z / magnitude
self.get_logger().info(f"Vector3\n\tx: {data.x}\ty: {data.y}\tz: {data.z}")

Lastly, define the main function:

def main(args=None):
rclpy.init(args=args)
node = TutorialSubscriber()

try:
rclpy.spin(node)
except KeyboardInterrupt:
print("\nKeyboardInterrupt received, shutting down...")
finally:
node.destroy_node()
if rclpy.ok():
rclpy.shutdown()

if __name__=="__main__":
main()

Updating package.xml

Before building the package, all dependencies for ROS2 packages and system libraries in all nodes in the package must be added to package.xml. Below the license line, add a build_depend tag and exec_depend tag for each dependency that is a ROS2 package or a system library.

For the intro_to_ros package, you only use ROS2 packages; numpy and random are Python3 libraries, so they do not need to be added.

  <build_depend>rclpy</build_depend>
<build_depend>geometry_msgs</build_depend>

<exec_depend>rclpy</exec_depend>
<exec_depend>geometry_msgs</exec_depend>

Updating setup.py

Before building the package, all executables must have an entry point in setup.py. Add the following lines within the square brackets in entry_points={'console_scripts': [],}:

'publisher = intro_to_ros.publisher:main',
'subscriber = intro_to_ros.subscriber:main',

The variable names in this case define the name of the built executable. intro_to_ros.publisher.main specifies the package and the name of the executable in the src directory.

Build the Package

The intro_to_ros is now configured properly to build the publisher.py and subscriber.py nodes. In a terminal on the backseat, build the package:

cd ~/auvc_ws
colcon build --packages-select intro_to_ros --symlink-install

A warning may appear saying that easy_install is deprecated since the --symlink-install argument is specified, but this warning can be ignored.

Since this is a new package, source the workspace's install file:

source ~/auvc_ws/install/setup.zsh

Testing the Publisher Node

Let's test the publisher node. In a terminal on the backseat, start an instance of publisher.py:

ros2 run intro_to_ros publisher

In the terminal, you should see the x, y, and z components of a pseudo random vector being printed every 1.0 seconds. Shutdown the node using CTRL + C or CMD + C.

Testing the Subscriber Node

Now, let's test the subscriber node. In a terminal on the backseat, start an instance of subscriber.py:

ros2 run intro_to_ros subscriber

Initially, the node will only log that it is started. Since there are no messages on the topic when only the subscriber node is running, the callback function does not run.

To test the subsriber, publish a single Vector message. Open a new terminal on the backseat while the subscriber node is running. In that terminal, enter the following command:

ros2 topic pub /tutorial/vector3 geometry_msgs/Vector3 '{x: 1.0, y: 1.0, z: 1.0}'

Look at the terminal where the subscriber node is running. You should find that the node logged a unit vector with equal components.

Testing the Publisher/Subscriber Pair

Now that you have confirmed each node works independently, run an instance of each node in two seperate terminals on the backseat.

Check-off

Review with a TA or instructor to check-off this section.

Problem Set

Problem Set Setup

Installing ArduSub

MAVROS is a ROS2 package that enables communication between flight control softwares like ArduSub and the ROS2 environment. For the purposes of this problem set and future simulations, you must install ArduSub. When you interface with the ROVs themselves, you'll connect to a SITL running onboard and therefore you will not need to run your own.

Install ArduSub from the official Git repository on the backseat.

cd && git clone --recurse-submodules https://github.com/blksail-edu/ardupilot
cd ~/ardupilot

Create a new virtual environment for ArduSub and install the following dependencies:

python3 -m pip install empy==3.3.4 pexpect mavproxy future dronecan setuptools

Also install the ccache system library:

sudo apt-get install ccache

Now, configure ArduPilot:

./waf configure
. ~/.profile
./waf clean

On the backseat, use the following commands in a terminal to add ArduPilot to path:

echo "export PATH=$PATH:$HOME/ardupilot/Tools/autotest" >> ~/.zshrc
echo "export PATH=/usr/lib/ccache:$PATH" >> ~/.zshrc

Next, install the gcc-arm cross-compiler:

cd ~/ardupilot && mkdir TARGET_DIR && cd TARGET_DIR
wget -c https://firmware.ardupilot.org/Tools/STM32-tools/gcc-arm-none-eabi-10-2020-q4-major-x86_64-linux.tar.bz2

Unpack the compiler:

tar -xjvf gcc-arm-none-eabi-10-2020-q4-major-x86_64-linux.tar.bz2

Add the compiler to path on the backseat using the following terminal command:

echo "PATH=$PATH:$HOME/ardupilot/TARGET_DIR/gcc-arm-none-eabi-10-2020-q4-major/bin" >> ~/.zshrc

Configuring MAVROS

Before you can use ArduSub, you must finalize the MAVROS installation. We have preinstalled MAVORS on your Raspberry Pi, but you must fun the following commands to install an additional library:

cd && wget https://raw.githubusercontent.com/mavlink/mavros/ros2/mavros/scripts/install_geographiclib_datasets.sh
chmod +x ./install_geographiclib_datasets.sh
sudo ./install_geographiclib_datasets.sh

Launching ArduSub

Now, in a terminal on the backseat, start a SITL with the following command:

~/ardupilot/Tools/autotest/sim_vehicle.py --vehicle=ArduSub --aircraft="bwsibot" -L RATBeach --out=udp:YOUR_COMPUTER_IP:14550
Note

The first time you start the SITL, it may take some time to configure.

Launching MAVROS

Once the SITL is running, you can connect to it by launching MAVROS in another terminal on the backseat:

ros2 launch mavros apm.launch fcu_url:=udp://127.0.0.1:14550@14555 gcs_url:=udp://:14550@YOUR_COMPUTER_IP:14550 tgt_system:=1 tgt_component:=1 system_id:=255 component_id:=240
Note

When you run this command on the backseat, MAVROS may warn you that RTT is too high for timesync.

Note

The above ros2 launch command only connects to the SITL running locally on the RPI. You will use different arguments to connect to the BlueROV2 vehicles.

Getting Your Computer's IP

To get your computer's IP address,

With the backseat connected to your Mac, then go to System Settings and select Network. Find the RNDIS/Ethernet Gadget. Once that interface has a yellow light, click on it. Use the IP address field.

Problem One

With both ArduSub and MAVROS running on the backseat, you can access simulated messages from the BlueROV2 vehicle in the ROS2 environment.

In your intro_to_ros package, create a new node called bluerov2_sensors. This node should do the following:

  1. Subscribe to the BlueROV2's battery sensor messages.
  2. Subscribe to the BlueROV2's inertial measurement unit (IMU) sensor messages.
  3. Have a class attribute for the battery message type.
  4. Have a class attribute for the IMU message type.
  5. Update the battery attribute to the latest battery sensor message when it is received.
  6. Update the IMU attribute to the latest IMU sensor message when it is received.
  7. Create a timer and a corresponding method to check whether the battery has fallen below a safe voltage. If it has, log a message to the terminal. This method should run every 5.0 seconds.

Update package.xml and setup.py to account for your new node, then rebuild the intro_to_ros package.

Problem Two

Open QGroundControl on your Windows or macOs machine. You should see the the BlueROV2 on the United State's west coast. Modify your bluerov2_sensors node so that it prints the battery voltage at each callback, then, run your node.

In QGroundControl, arm the ROV and change the flight mode to GUIDED. Then, click on a point anywhere else in the water and select Go to location. After holding down space or dragging the slider, you should see the ROV start moving.

Now check the terminal where your node is running. What do you observe about the battery's voltage?

Check-off

Review with a TA or instructor to check-off this section.

Problem Three

Create a new node called 'physics_sim'. This node should do the following:

  1. Subscribe to Pose2D messages on the topic named /physics/pose2d`.
  2. Have a method that runs your simulate_auv2_motion function from Problem 10 in Underwater Physics.
  3. Have a method called posed2d_callback that receives Pose2D messages. When you receive, run the simulate_auv2_motion using x0=msg.x, y0=msg.y, and theta0=msg.theta. For the remaining inputs, use the following values (not accurate to the BlueROV2):
  • T = np.ndarray(shape=(4,), dtype=float, buffer=np.array([2.0, 1.0, 2.0, 1.0]))
  • alpha = np.pi / 4
  • L = 0.3
  • l = 0.2
  • mass = 1.0
  • dt = 0.1
  • t_final = 10.0
  1. Save the plots as .png file(s) on the backseat.

Add any additional attributes or methods as needed. Update package.xml and setup.py to account for your new node, then rebuild the intro_to_ros package.

For this problem especially, use the --symlink-install argument when you build the package. This way, if your node does not work as intended, you do not have to recompile the package when you make changes to physics_sim.py.

With your node running on the backseat, open a new terminal and run the following command:

ros2 topic pub -1 /physics/pose2d geometry_msgs/Pose2D '{x: 1.0, y: 1.0, theta: 0.5}