Skip to content

ROS Fundamentals

Yashas Ambati edited this page Sep 18, 2024 · 35 revisions

Welcome to software training! This page contains materials that we will use to train you on the fundamentals of ROS 2. We'll start out with some theory, and move into more hands-on training. We encourage reading this document in its entirety.

Tip

Go to https://docs.ros.org/en/humble/index.html. Bookmark it. You'll use it a ton.

What is ROS?

ROS stands for Robot Operating System. However, contrary to the name, ROS is not an operating system! Rather, it is a set of software libraries, developer tools, and introspection utilities that help us build robust robot applications.

Perhaps the most important thing that ROS gives us is the ability to organize our software into a group of "subsystems" called nodes that send and receive data between themselves (called messages). According to the ROS documentation, nodes serve a singular, modular purpose. For example, publishing camera data or sending command velocities to firmware.

Important

You've probably noticed something. We have nodes that send/receive messages between themselves. We can say that two nodes that communicate with each other have an edge between them. So that means ROS is structured like a graph! You'll likely hear this terminology a lot.

Consider the following image. ROS is comprised of a bunch of nodes that communicate and process data in real time. We'll go over what "topics" and "services" are next.

ROS Graph

What is a topic?

A topic acts as a "mailbox" for nodes to exchange messages. A node can read from and write to any number of topics. When a node reads from a topic, we say that it subscribes to that topic. When a node writes to a topic, we say that it publishes to that topic.

ROS Topics

Let's use an (imperfect) analogy to understand this. You can think of radio transmitters as nodes, and radio frequencies as topics. Just like how a radio transmitter broadcasts a signal to a specific frequency, a node publishes messages to a specific topic. And just like how a radio receiver can tune in to a specific frequency to receive the signal, a node can subscribe to a specific topic to receive messages. Multiple nodes can publish and subscribe to the same topic, just like how multiple radio transmitters can broadcast to the same frequency and multiple receivers can tune in to that frequency to receive the signal.

Extended reading: https://docs.ros.org/en/humble/Tutorials/Beginner-CLI-Tools/Understanding-ROS2-Topics/Understanding-ROS2-Topics.html.

What is a service?

Surprise! Topics aren't the only way that nodes can communicate. Remember, topics operate on a publisher-subscriber model. However, services operate on the request-and-response model. The ROS 2 documentation probably puts it best.

While topics allow nodes to subscribe to data streams and get continual updates, services only provide data when they are specifically called by a client.

ROS Service

Note that there can be many clients nodes using a service, but only one server node providing that service.

ROS tools

ROS isn't just a message passing framework! It's complete with some crazy useful developer tools that make debugging a ROS system easier. We'll go over some of those tools in this section.

RViz

RViz (short for ROS Visualization) is a 3D visualization tool that allows you to understand the state of the robot in realtime. It is not a simulator. RViz simply allows you to subscribe to any number of topics and visualize the data that is being published on them. This includes raw sensor data, joint states, elevation maps, etc. For example, if the state of the robot in the simulator looks like this:

Gazebo View

Then the real-time, visualized perception and planning outputs can be visualized in RViz like this:

RViz View

There are a few things to notice in the screenshot of RViz. The displays panel on the left shows what data we are visualizing. You can see the datatype of each topic we're visualizinghttps://docs.ros.org/en/foxy/Concepts/About-Logging.html, and the actual topic path. For example, in this screenshot we're visualizing the costmap in real-time. Thus, we have a Map type display and set the topic to /costmap.

How do you add a new display?

Easy! Simply click the "Add" button at the bottom left of the displays panel. This will display a modal with two tabs that look should look something like the following. This allows you to either add a display by type, and manually set the topic in the displays panel or add a display by topic, which automatically infers the type of the display. More often then not, the by topic tab is what you'll use.

RViz add panel, by display type RViz add panel, by topic

Gazebo

In the previous section, you saw an image of our rover in simulation. We use Gazebo to simulate our robot and terrain! Gazebo is a high-fidelity physics and sensor simulator that plugs seamlessly into ROS.

Read about the full list of features Gazebo provides here: https://gazebosim.org/features.

image

Writing a ROS package

You're now ready to write some code! But, what is a package? The ROS 2 documentation defines it as the most basic "organizational unit" of ROS code. A package may contain one or more ROS nodes, a ROS-independent library, or anything else that constitutes a useful module.

Note

From the ROS documentation: In general, ROS packages follow a "Goldilocks" principle: enough functionality to be useful, but not too much that the package is heavyweight and difficult to use from other software.

Creating a package

Each ROS package must contain a minimum set of contents to be considered a package. These are:

  • CMakeLists.txt — This file describes how to build the code in your package. Specifically, it contains a set of directives that describe the libraries, source files, and dependencies needed to build the package.
  • include/ — This directory contains the public header (.hpp) files for the package.
  • package.xml — This file contains some metadata for the package that colcon (the ROS build tool) should know. The most important part is what other packages this package is dependent on. This helps with automating packaging.
  • src/ — This directory contains the source (.cpp) files for the package.

Thus, the structure of a simple package could look like this.

 my_package
 ├── CMakeLists.txt
 ├── package.xml
 ├── src
 │   └── my_node.cpp
 └── include
     └── my_node.hpp

While we could build out this directory structure by hand, there's a handy CLI tool that we could use to do this for us!

ros2 pkg create PACKAGE_NAME --build-type BUILD_TYPE --dependencies DEPENDENCIES --node-name NODE_NAME

Important

This command should be run inside of a ROS workspace's src/ directory. While colcon searches all sub-directories of a workspace to find packages, it's best practice to put them in a directory called src/. For the sake of this exercise, run this in the workspace you set up for our codebase.

Let's break this down.

  • The --build-type flag should be either ament_cmake or ament_python depending on if the package will contain C++ or Python source code.
  • The --dependencies flag can be used to list package dependencies. Specifying this option will automatically add the appropriate find_package directives in CMakeLists.txt and <depend> tags in package.xml.
  • Finally, the --node-name flag can be used to automatically add an empty executable file to src/ with the specified name.

Note

A full list of options can be seen by running ros2 pkg create -h in your terminal. The -h flag can be used with almost any ros2 command to get more information about its usage. If you're confused on how to use a command, -h is your best friend.

Adding a node

Let's create our first node. First, create a package with the following command:

ros2 pkg create --build-type ament_cmake --node-name greeter chatter

This should create a package called chatter with a source file greeter.cpp created in the src/ directory. Here are some requirements for the simple node that we're going to create.

  • Must subscribe to topic /name. Another node will publish the name of a user to this topic.
  • Must publish the string Hello, [name]. Nice to meet you! to topic /greeting

In general, we always write a header (.hpp) file for each source file. This does not get automatically created when you use the --node-name flag, so you have to create it! At the root of your package, run touch include/greeter.hpp and rm -r include/chatter. This will create greeter.hpp and remove the extra chatter/ directory that was generated.

Copy the following "skeleton" into greeter.hpp. Remember, header files generally contain declarations. A copy of the header file can then be directly inserted into a .cpp file that requires the declarations using the #include preprocessor directive.

#ifndef GREETER_HPP_
#define GREETER_HPP_

#include <rclcpp/rclcpp.hpp>

namespace chatter
{

class Greeter : public rclcpp::Node
{
public:
  explicit Greeter(const rclcpp::NodeOptions & options);
  ~Greeter();

private:
  std::string name_;
};

} // namespace chatter

#endif // GREETER_HPP_

Note a few things here.

  • When creating a node class, you must inherit from rclcpp::Node and declare a public constructor and destructor for the class. This is the single point of entry for creating publishers, subscribers, servers, and clients.
  • We make use of header guards to prevent duplicate definitions. See this link for more information.
  • We nest our node class within a namespace. While namespaces are primarily used to prevent naming conflicts in large projects, we mostly use them for organization purposes. Generally, we define a namespace with the same name as the package.

Warning

You may be tempted to use the using namespace directive for a namespace from which you use a lot of functions. For example, std. Do not do this! See this link if you're curious as to why.

Now, copy the following snippet into greeter.cpp.

#include "greeter.hpp"

namespace chatter {

Greeter::Greeter(const rclcpp::NodeOptions & options) : Node("greeter", options)
{
  RCLCPP_INFO(this->get_logger(), "Greeter node has been started.");
}

Greeter::~Greeter() {}

} // namespace chatter

#include <rclcpp_components/register_node_macro.hpp>
RCLCPP_COMPONENTS_REGISTER_NODE(chatter::Greeter)

The most important thing here is the RCLCPP_COMPONENTS_REGISTER_NODE macro. Calling this macro allows you to register your node class as a component which can dynamically be loaded into a process at runtime. This idea falls under the ROS "Composition" paradigm. If you're curious, you can read more about it here.

In the definition of the constuctor, we call the super constructor of rclcpp::Node. We also call RCLCPP_INFO() to log the string "Greeter node has been started" to the console when the node is constructor. The first parameter must be a rclcpp::Logger object. Generally, we use the node's logger object which can be accessed using this->get_logger().

Note

From the ROS documentation: Log messages have a severity level associated with them: DEBUG, INFO, WARN, ERROR or FATAL, in ascending order. Each node has a logger associated with it that automatically includes the node's name and namespace.

Calling RCLCPP_INFO() will log a message with the INFO severity level, calling RCLCPP_DEBUG() will log a message with the DEBUG severity level, and so on. Read more about logging in ROS 2 here.

At this point, your package should look like this:

 my_package
 ├── CMakeLists.txt
 ├── package.xml
 ├── src
 │   └── greeter.cpp
 └── include
     └── greeter.hpp

Now, navigate to the top-level directory of your workspace and build your package using colcon build --symlink-install --packages-up-to chatter. You should get an error! That's because we haven't defined the correct dependencies and build rules in our CMakeLists.txt and package.xml files! Let's do that now.

If you didn't get an error... well, let's just say you should definitely go buy a lottery ticket or something! Please come and talk to us so we can figure out why you didn't get one.

Copy the following code into your CMakeLists.txt file.

cmake_minimum_required(VERSION 3.8)
project(chatter)

find_package(rclcpp REQUIRED)
find_package(rclcpp_components REQUIRED)
find_package(ament_cmake REQUIRED)

include_directories(
  include
)

add_library(${PROJECT_NAME} SHARED
  src/greeter.cpp
)

set(dependencies
  rclcpp
  rclcpp_components
)

ament_target_dependencies(${PROJECT_NAME}
  ${dependencies}
)

rclcpp_components_register_node(
  ${PROJECT_NAME}
  PLUGIN "chatter::Greeter"
  EXECUTABLE ${PROJECT_NAME}_Greeter
)

ament_export_include_directories(include)

ament_export_libraries(${PROJECT_NAME})
ament_export_dependencies(${dependencies})

ament_package()

CMakeLists.txt are written using CMake. CMake is a powerful set of tools for managing the software build process.

ROS CLI

The best way to learn about the ROS CLI is by going through https://docs.ros.org/en/humble/Tutorials/Beginner-CLI-Tools.html. However, this section provides a "snapshot" of the most important commands.

There's a lot of cool ROS command-line tools that you'll run into as you develop ROS software. These are just the most important ones.

Command Usage
ros2 topic list This command is used to list all ROS 2 topics that are currently being published or subscribed to. If you want more information about a specific topic, ros2 topic info <topic_name> is your friend.
ros2 topic echo <topic_name> This command is used to print data that is currently being published to topic_name in real-time.
rqt_graph This one is super cool. It brings up a visualization of the current ROS graph. This shows all currently running nodes, services, and the connections between them.
colcon build --symlink-install This command builds all of our robot code. Why --symlink-install? This flag allows you to modify python code without having to re-build everything, and leads to better error linking for C++ files. Another cool flag is --packages-up-to <package_name>, which will build a single package and all of its dependencies. Other useful package-related flags can be found here.