Learning ROS2 (Robot Operating System 2)
Despite its name, ROS2 is not actually an operating system. Rather, it is a set of software libraries and tools that help you build robot applications. As its name suggests ROS2 is the big brother to ROS. I believe learning ROS2 will be much easier than ROS as there is more documentation and more unity (in terms of how to do things). Jumping into ROS2 can be a bit intimidating for beginners, but don’t worry! Once you wrap your head around it, its core concepts become fairly intuitive. We will attempt to present these concepts in a simple manner, then we will provide links to tutorials that will further explain the concepts and walk you through writing some simple programs. Make attention to these and make sure to try writing them yourself, and experimenting with things, because we have found that the best way to learn ROS2 is to immerse yourself in it. After the tutorials, we will present a challenge for you to complete.
ROS2 Installation
Robot Operating System 2 (ROS2) is a framework for programming robots that we use as the backbone for our software architecture. Despite the name, it is not truly an OS, but actually a set of packages installed over Ubuntu that aid robotic software development by simplifying communications between processes and providing common functionality so we don’t have to reinvent the wheel. The version of ROS2 we use is called galactic (please check with the software lead or senior member that this is the version we are using for our current rover). Before getting started with ROS, first install it by following the official tutorial: https://docs.ros.org/en/galactic/Installation/Ubuntu-Install-Debians.html
ROS2 Basic Concepts
The challenge of programming an entire robot is that it quickly becomes hugely complicated. Between firmware to control your sensors and actuators, and the logic to perform various functions, if you tried to code everything at once you would end up a) reinventing the wheel on many components, and b) end up with a hugely messy program that probably won’t adapt well to changes in objectives, and any changes you make will likely break other things. When tackling such a challenge it is important to decompose your problem into smaller subproblems which can be self-contained. This way each sub-problem can be solved more simply, and the system gains modularity because you can swap out subcomponents without affecting other parts.
One intuitive way to split up your software is to break each sub-component into different programs (processes). For example, one program controls the wheels, one reads from your sensors, one figures out how to navigate autonomously given the surroundings, and another controls the arm joints. Seems promising, but how do these processes communicate? Inter-process communication is a must, because your navigation process needs to get the camera readings from your camera process, and needs to communicate the desired wheel actions to your wheel controller process. Inter-process communication is non-trivial to develop, but this is where ROS2 comes in to save the day.
ROS2 provides a framework to organize your robot into separate programs which tackle different sub-problems and allows easy communication between these programs in the form of messages.
Let’s introduce some of the basic terms that are used in ROS2, then explain what they are and how they fit together:
- Node: A program that uses ROS2.
- Message: A message containing data being sent by one node to another
- Message Type: Each message has a type, which determines what kind of data it contains
- Topic: Messages are sent to topics, and nodes can listen to the topics they are interested in to receive messages sent to that topic
- Publisher: Nodes can publish messages to a topic
- Subscriber: Nodes receive a message from a topic by subscribing to it.
When developing with ROS2, you break your system down into nodes, which are simply programs that use ROS2. A node can send and receive messages, as well as use various other ROS2 functionalities. When nodes need to communicate, they send messages to each other. A message is a chunk of data that gets sent between nodes. Each message has a message type, which dictates what data can be stored in that message. There are hundreds of message types available, ranging from simple ones containing just a single number to complex ones containing, for example, images, GPS coordinates, or 6-dimensional velocities. If ROS2 doesn’t have an appropriate message type for your needs, you can even define new message types.
Now things get slightly more complicated. Say Node A wants to send a message to Node B. You might think that it would create the message, fill in the data, and send the message directly to Node B. This isn’t quite how things work though. First of all, consider the modularity of the system. If Node A sends the message specifically to Node B, then what happens if Node B is swapped out to an alternate Node C? Or, what if Node A doesn’t know who it's sending a message to, such as in the case where Node A is a driver that reads from a sensor and sends the readings to other nodes, and multiple nodes want to receive that data? For these reasons sending messages directly to a recipient is not how things are done in ROS2.
Instead, a ROS2 system has its messages organized into topics. A topic is just a name where messages are sent to, such as “/camera_image”, or “/gps_location” (topics always start with a slash). A node that is sending a message sends it to a specified topic. This is called publishing. A node wishing to send data publishes its messages to a topic. A node wishing to receive messages can then listen to the topic where the appropriate message is being sent, and get notified when a message is sent. This is called subscribing. Nodes subscribe to a topic, and when a message is published to that topic, they will receive it.
This helps keep the system modular, as nodes can be swapped out as long as they know what topics to subscribe/publish to, and multiple nodes can subscribe to the same topic. ROS2 manages all the details under the hood, making our lives easier. For example, a node acting as a driver for a GPS sensor can publish messages to “/gps_location” regardless of who’s subscribing, and any node requiring location info can subscribe to “/gps_location”.
Diving a little further: ROS2 Services
An important note is that publishing and subscribing in ROS2 is asynchronous. This means that after a node publishes a message, its execution will continue onto its following lines of code without waiting for other nodes to receive the message. A consequence of this is that it is impossible to know whether the message was received by another node. If synchronous behavior is required, then it is better to use ROS2 services instead of messages.
A service is a synchronous way of communicating between ROS2 nodes. In this way of communicating, one node acts as a server, and another node acts as a client. When the client wants to interact with the server, it calls the server’s service. This means that it sends a message (request) directly to the server, and blocks until it receives a reply. The server receives the request in a similar way to receiving a message, but it must give a response to the client. Both the request and response can be any ROS2 message type, including custom ones, so the possibilities are endless. Previously, I mentioned service is a synchronous way of communicating between ROS2 nodes - although this is true in ROS1- it is not the complete truth in ROS2. In ROS2 (if we so choose) we can make the client asynchronous (non-blocking).
Should you use a publisher/subscriber for your code or a service? Typically you can make either work, but usually, one solution will be more elegant. That being said, a best practice and general rule of thumb is that it is recommended to communicate with downstream nodes with service calls (with responses usually just being acknowledgment), and communicating upstream by publishing messages. This is because an upstream node will often be useless without downstream nodes it communicates with, and should probably not run if they are not present. Calling their services will fail since they don’t exist. A downstream node however should be agnostic of its upstream nodes, and should therefore not get blocked by waiting on upstream services. Here is a simple example of what I mean.
Suppose you have one node interfacing with your robot’s motors, and another doing path planning. The motor driver receives motor commands and performs them, and provides feedback on wheel odometry (how much each wheel has moved). The path planner performs logic to determine how it wants the robot to drive and sends the commands to the motor driver. The motor driver should be agnostic to nodes using it, so it should publish its odometry, which nodes can subscribe to. Nodes sending commands to the motors, however, are useless if the motor is not running, so it may make sense to send motor commands via a service call.
ROS2 Parameters
Another nice thing about ROS2 is the ability to tweak the values of parameters rather than hard-coding them and recompiling if a number needs to change. You can create config files (typically a YAML file) defining values for parameters you’d like, which can be loaded on launch. Then your node can read parameters by name in order to set values (eg scales, thresholds, topic names, speed/joint angle limits, etc). Parameters read from files are hosted on that current node (As compared to ROS1 where there was a parameter server). A nice feature in ROS2 is the ability to declare parameters from within our nodes (fairly easily) that can then be modified from a configuration file. Keep in mind that if other things modify the parameter, it's up to the programmer to catch changes in the value and deal with them. Keep an eye out for parameters in the tutorials linked below.
ROS2 Components & Nodes
In ROS1 there was this notion that a node was unique to a single process. Thus, each individual node that was running had its own process ID (AKA pid). However, in ROS2 with the development of this thing called a “component” we are able to have a ROS2 node dynamically loaded at runtime as a shared library. With this in mind, there can be multiple ROS2 components within the same process. Thus, this allows for the modular development of code that will allow one to break down a problem into different parts (you could imagine as components). In addition to having standalone components in ROS2, we are allowed to have multiple nodes within the same process. This is accomplished by using something called a Multi-threaded executor in ROS2 or a Single-threaded executor (have nodes on one thread or multiple threads - developer’s choice). I will have links that show them being used towards the end of this document. From a syntactical standpoint, a ROS2 component just has a macro (typically at the end of the source file) that is really just a plugin that allows the class to be loaded dynamically at runtime.
ROS2 Build System
As it stands right now the default ROS2 build system is colcon build. For the next little bit on the software team we will be using colcon build. Do be sure to run colcon build in the top level of your workspace to properly build all the ROS2 packages
ROS2 Tutorials
Go through these tutorials to get a better understanding of ROS2. They are pretty thorough and cover almost everything you need to know. Although there is much more to ROS2 - the idea behind this guide is not meant to overwhelm you but to serve as a guide. There are many neat things in ROS2 such as topic statistics, life cycle nodes, quality of service, and callback groups. In the effort of making this training as simple and straightforward as possible, I will not explore these concepts here. The tutorials I have attached below are HIGHLY RECOMMENDED to be completed before you move on to completing the software training challenge. In fact, I would go far enough to say it is (more) difficult to complete the software training challenge without the ROS2 foxy tutorials. These tutorials are helpful, but you do not need to go through all of them. Only go through the ones that you believe will help you best.