ROS Framework and Concepts
Robot Operating System (ROS) is an open-source operating system tailored specifically for robotics. In Part 1, Raul discusses the basic concepts about the ROS framework and how it’s used. This is an introductory-level discussion based on the more established ROS version 1.
In Part 1 of this three-part article, I begin by explaining introductory concepts about the Robot Operating System (ROS) framework—what it is and what it is used for. I also discuss ROS basic notions grouped into three levels: The Filesystem Level, the Computation Graph Level and the Community Level. Next, I talk briefly about what to do to get ROS installed in an Ubuntu machine, and I also explain how to build a simple ROS package from the ground up, with an example intended to help reinforce the understanding of the discussed basic concepts.
Because this is introductory-level material geared toward beginners, I focus exclusively on ROS (version 1), though ROS2 (version 2) is currently available. I think version 1 is still the most convenient way of learning ROS for beginners, because in contrast to ROS2, there’s a vast amount of information and learning resources, as well as third-party packages for several commonly used sensors, actuators and algorithms. Besides, it has more practical examples for many types of robots and systems than does the newer ROS2.
In Part 2 of this article, I will take all that’s explained here a step further, to discuss the build of a ROS robotic car that can be used for further learning and experimentation. This article is aimed at students and robotics enthusiasts without any previous experience with ROS. However, basic knowledge of the Linux command line, and basic experience with the Python programming language are required to follow the presented content.
WHAT IS ROS?
The official ROS Wiki site defines it very clearly: “ROS is an open-source, meta-operating system for your robot. It provides the services you would expect from an operating system, including hardware abstraction, low-level device control, implementation of commonly-used functionality, message-passing between processes, and package management. It also provides tools and libraries for obtaining, building, writing and running code across multiple computers“[1].
The development of robust robotic systems requires a base framework of tools, services, hardware abstraction, data exchange and other general functionality, which are common to the majority of robotic applications. ROS is meant to help jump-start robotic applications by providing this base framework, so we can more rapidly get to the point at which we care more about our specific application, and get a working prototype quickly.
In this article, I focus exclusively on ROS (version 1), because I think that, at least for now, it’s the best way to begin learning ROS. Some may disagree in favor of ROS2 for completely valid reasons; however, once you learn ROS, it will be easy to learn ROS2. Although both versions can be quite different in some aspects, the general idea is the same. Most of the information presented here was distilled from the ROS Wiki site [2], with the goal in mind of going from basic concepts to building a simple ROS package reasonably fast. Credit for the original content goes to the publishers of the ROS Wiki site.
— ADVERTISMENT—
—Advertise Here—
The ROS Wiki site summarizes ROS main concepts or “notions” into three levels: The Filesystem Level, the Computation Graph Level and the Community Level. Let’s review each one of them.
THE FILESYSTEM LEVEL
Figure 1 is a relational diagram of the concepts grouped in the Filesystem Level. Let’s explain them a bit.
Packages: These are the main units into which ROS software is organized. Packages are reusable modules encapsulating a given functionality, such as access to a video camera, or a steering controller for a robotic car, a package for robot localization or path planning, for computing kinematics or doing linear algebra calculations, for viewing sensor signals, controlling a robot with a keyboard or joystick and controlling a drone with the MAVLink protocol, to name a few examples. Packages can contain ROS “nodes” (more on these later), software libraries, datasets, configuration files and so on that enable the package’s functionality. ROS packages are meant to be reusable. They must be lightweight and very specific, and easily integrated with other software. A package is the smallest entity in ROS that can be built (compiled), and it is the way software is shared or bundled to be used in robotic systems. In the computer’s file system, a ROS package is simply a folder with files, and a package manifest file named package.xml.
Metapackages: These are specialized ROS packages that contain only a package manifest (a type of file discussed below)—but no other files, such as code, tests or other items. Metapackages are used to group and represent related packages. In other words, they are “packages of packages,” which are useful, for instance, when we need to install a set of related packages.
Package manifests: The package manifest is an XML file called package.xml that goes inside any package or metapackage’s root folder. It contains metadata that define the package’s general properties, such as the name, version, authors, maintainers and dependencies on other packages. Dependencies are key to the correct installation and function of the package. In this context, a “Repository” is a collection of packages sharing a common Versioning Control System (VCS). Sometimes, a Repository can contain only one package.
Message (msg) types: Messages are the way nodes, which are subsystems in a robot, in ROS exchange data (explained later). They may be in the form of primitive types, such as numbers (integers, floats and so on), strings, Booleans or more complex compound types. Messages use a simplified description language to characterize data. Such descriptions are stored in .msg files in the msg/ subdirectory of ROS packages, and are referred to by prefixing them with their package names. For instance, the sensor_msgs/CameraInfo is a CameraInfo message type from the sensor_msgs package that defines meta information for a camera.
Service (srv) types: Services in ROS enable request/response communication between nodes. For instance, the sensor_msgs/SetCameraInfo service can be used to request a camera to store camera calibration information. Services also use a simplified service description language that builds directly upon the previously discussed ROS msg format and are stored in .srv files inside the srv/ subdirectories of packages. Services are also referred to by prefixing them with their package name. For instance, the SetCameraInfo service belongs to the sensor_msgs package.
THE COMPUTATION GRAPH LEVEL
The ROS Computation Graph Level is the network of all ROS tasks in a robotic system processing and exchanging data among those tasks. Figure 2 is a relational diagram of the concepts in this level.
Nodes: A node is a modular and specialized entity that performs some kind of computation. For example, in a robotic car there could be a node in charge of controlling the motors, another in charge of accessing the video camera, another running object detection with computer vision, another performing path planning, another uploading data to the cloud and so on. A ROS robotic system typically consists of many nodes connected in the computation graph network, exchanging data via topics, Remote Procedure Call (RPC) services and the Parameter Server. This modularity reduces and compartmentalizes code complexity, isolates fault tolerance, abstracts unnecessary implementation details and exposes a minimal API to the rest of the graph.
— ADVERTISMENT—
—Advertise Here—
Nodes have unique resource names defined within a namespace that can be shared with other resources. For example, raspicam_node is the name of a driver ROS node to access Raspberry Pi cameras. ROS nodes can be written using the main client libraries: roscpp (for C++), rospy (for Python) and roslisp (for Lisp), along with some experimental client libraries for other programming languages [3].
Master: The ROS Master is in charge of registering all nodes in the system, so they can locate each other and communicate. It also keeps track of all available topics with their associated publishers and subscribers, and all available services in the system. It also provides the Parameter Server. The Master must be run first, so nodes can communicate and exchange messages or call available services.
Parameter server: The Parameter Server is used to store configuration parameters that reflect the configuration state of the system. It runs inside the Master. Nodes can store and retrieve parameters at runtime, to inspect and modify the system’s configuration state. Parameters are stored as a dictionary of multiple values accessed by key.
Messages: Nodes exchange data by passing messages. Any node can publish to more than one topic, and read messages from more than one topic as well. Let’s consider a simple example: a joystick_command publisher node publishes messages to a /command topic, from which a motor_driver subscriber node can read them. Those messages could be, for instance, “forward,” “left,” “right” and so on, to drive a robotic car in a given direction. Figure 3 shows this communication scheme.
Topics: Topics are communication channels with unique names that identify the type of messages they carry. Nodes that produce data publish them as messages to a topic, so other nodes can read and process them. Nodes interested in a given topic subscribe to it to receive the published messages in that channel. Multiple nodes can publish to the same topic at the same time, and multiple nodes can subscribe to a topic as well. Publishers and subscribers are generally anonymous to one another. There’s no way for a node to know which publisher is the origin of a received message, or which nodes will receive a published message.
Services: A Service is a string-named Remote Procedure Call (RPC) request sent by a client to a host node, which returns a reply with the result of the requested service.
Bags: A Bag is a ROS file format (with the .bag file name extension) for storing message data that can be saved and played back later. It allows, for instance, the collection and storage of sensor data to be replayed later, to test algorithms in the development process. There are many tools for storing, processing, analyzing and visualizing Bag data.
Next is the Community Level. Resources in this level enable the exchange of software and knowledge. These resources include: Distributions, Repositories, the ROS Wiki, a Bug Ticket System, Mailing Lists, the ROS Answers website and the ROS Blog. Sooner or later, you’ll have to know what the Community Level has to offer in terms of software availability and support [2]. However, here we want to concentrate mainly on building a ROS package from the ground up, while gaining a fair understanding of the main functional concepts, so I will skip them for now.
BUILDING A ROS PACKAGE
Installing ROS is relatively painless when using the Ubuntu operating system. To write and test the example presented here, I installed ROS Noetic Ninjemys on Ubuntu 20.04. I won’t cover the installation process in detail here, but the file install_run.md that comes with the source code for this article contains the step-by-step procedure, which is also available at the ROS Wiki page [4]. If you don’t have ROS already installed, follow those instructions to install it.
Now, to deepen our understanding of the concepts explained before, let’s build a basic ROS system with two nodes—one publisher and one subscriber. We will call the publisher command_node. It will publish messages to a topic called /command to control a fictional robotic car. Five control commands will be available: forward
, backward
, right
, left
and stop
. We will call the subscriber drive_node, and it will subscribe to the /commands topic to read the commands and control the robotic car accordingly.
Listing 1 shows the Bash commands we will execute in the Ubuntu terminal window to create and build our ROS package (assuming you already have ROS installed in your computer). First, by running cd ~/catkin_ws/src we go to the src folder inside the catkin_ws workspace folder and then we create the package first_example by running the command catkin_create_pkg first_example std_msgs rospy roscpp. The package first_example will depend on the packages std_msgs, rospy, and roscpp. We won’t write C++ code for this example, but we are adding roscpp as a dependency just to show how you should add it, in case you want to write nodes in C++ in the future.
# Create our custom ROS package
cd ~/catkin_ws/src
catkin_create_pkg first_example std_msgs rospy roscpp
# Create our scripts folder
mkdir first_example/scripts
cd first_example/scripts
# Create the two node scripts
touch command_node.py
touch drive_node.py
# Make the scripts executable
chmod +x command_node.py
chmod +x drive_node.py
— ADVERTISMENT—
—Advertise Here—
# Compile the ROS package
cd ~/catkin_ws
catkin_make
# Source the workspace
echo "source ~/catkin_ws/devel/setup.bash" >> ~/.bashrc
source ~/.bashrc
# Run in the current terminal window:
roscore
# Run in terminal window 2:
rosrun first_example command_node.py
# Run in terminal window 3:
rosrun first_example drive_node.py
LISTING 1 – Bash commands to build a ROS package
Next, with lines 6-7 we create a scripts folder inside the first_example folder and change the directory to that location. Next, we create two files inside this folder: command_node.py and drive_node.py (lines 10-11), one for each node. Listing 2 shows the content for command_node.py, and Listing 3 shows the content for drive_node.py. Now, open them with a text editor of your choice, and copy the code from the two listings into the two created files. Alternately, you can download the source code files accompanying this article and copy/replace them. Next, with lines 14-15 we make the files executable, with lines 18-19 we compile the package and with lines 22-23 we again source our workspace.
LISTING 2 – Python code for command_node.py
#!/usr/bin/env python3
import rospy
from std_msgs.msg import String
import sys, select, termios, tty
command = 'stop'
last_command = 'stop'
msg = """
Reading from the keyboard and publishing to /command!
---------------------------
Moving around:
i
j k l
,
CTRL-C to quit
"""
def talker():
global command
global last_command
pub = rospy.Publisher('/command', String, queue_size=10)
rospy.init_node('keyb_commander', anonymous=True)
rate = rospy.Rate(10) # 10hz
print(msg)
while not rospy.is_shutdown():
key_timeout = 0.6 # Seconds
k = getKey(key_timeout)
if k == "i":
command = 'forward'
elif k == ",":
command = 'backward'
elif k == "j":
command = 'left'
elif k == "l":
command = 'right'
elif k == "k":
command = 'stop'
elif k == '\x03': # To detect CTRL-C
break
if command != last_command:
print("Published command: " + command)
last_command = command
pub.publish(command)
rate.sleep()
def getKey(key_timeout):
tty.setraw(sys.stdin.fileno())
rlist, _, _ = select.select([sys.stdin], [], [], key_timeout)
if rlist:
key = sys.stdin.read(1)
else:
key = ''
termios.tcsetattr(sys.stdin, termios.TCSADRAIN, settings)
return key
if __name__ == '__main__':
settings = termios.tcgetattr(sys.stdin)
try:
talker()
except rospy.ROSInterruptException:
pass
LISTING 3 – Python code for drive_node.py
#!/usr/bin/env python3
import rospy
from std_msgs.msg import String
received_command = ''
last_received_command = ''
def listener():
rospy.init_node('motor_driver', anonymous=True)
rospy.Subscriber('/command', String, commandCallback)
rospy.spin()
def commandCallback(message):
global received_command
global last_received_command
received_command = message.data
if received_command == 'forward':
forward()
elif received_command == 'backward':
backward()
elif received_command == 'left':
left()
elif received_command == 'right':
right()
elif received_command == 'stop':
stopMotors()
else:
print('Unknown command!')
if received_command != last_received_command:
print('Received command: ' + received_command)
last_received_command = received_command
def stopMotors():
# Here goes the code for stopping the motors
pass
def forward():
# Here goes the code for driving the motors forward
pass
def backward():
# Here goes the code for driving the motors backward
pass
def left():
# Here goes the code for driving the motors left
pass
def right():
# Here goes the code for driving the motors right
pass
if __name__ == '__main__':
print('Ready to receive commands!')
listener()
print('Node is shutting down, stopping motors')
stopMotors()
Now we are ready to run our ROS system. In the current terminal window, run roscore
(line 26). This command will start some basic nodes and programs required for ROS to work, such as: the ROS Master, the ROS Parameter Server and the rosout
logging node (Figure 4). In a second terminal window, run the command_node.py
node with the rosrun
rosbash command (line 29). After that, you should see text in the command window with instructions on how to use the keys i, j, k, l and , to control the robotic car (Figure 5). Next, open a third terminal window and execute the command in line 32 to run the drive_node.py node, as well. You should see in this terminal window the welcome message “Ready to receive commands!” and stop
as the default published command (Figure 6).
Now, go back to terminal window 2, where command_node.py is running, and type i. You should see in the window the text message “Published command: forward.” Immediately after that, terminal window 3 should show the message “Received command: forward”. Try with other commands (j, k, l and ,) to see a corresponding behavior (Figure 7).
THE PUBLISHER NODE
Let’s explain how the Python code for the publisher node works (Listing 2). First, we import rospy
(line 2), which is the Python client library for ROS that allows us to interface with ROS topics, services and parameters by using the Python programming language. With line 3, we import the String
message type from the std_msgs package. This package contains wrappers for ROS primitive types, such as Boolean, integers, floats and so on, and of course, the String
type. We are importing the String
type because this node will publish commands as string messages (forward
, backward
, left
, right
and stop
). For better controllability we could publish the driving command as an array of two velocities: one linear in m/s and one angular in rad/s (both of type float
or double
), but we will keep it simple here for the sake of the example. Line 4 imports some Python modules that will allow us to seamlessly read key presses from the computer keyboard.
In lines 6-7 we declare two command variables and initialize them with the stop default value. In lines 9-18 we declare a string constant containing the welcome message that’s displayed in the terminal window after the node begins execution. Next, in lines 20-51 we define the talker()
function containing the node’s core instructions. In lines 21-22 we declare the variables command
and last_command
as global, so they locally refer to the global variables declared previously in lines 6-7. With line 24, we instantiate a “Publisher
” object named pub, which will publish a /command
topic of type String. In line 25 we initialize the node with the name keyb_commander
, and in line 26 we define the node’s execution rate (10Hz), which means the node will execute 10 times a second. Line 28 prints the welcome message, and lines 30-51 define an infinite loop that reads keystrokes from the computer’s keyboard and publishes command messages to the /command
topic.
Let’s see how this works. In line 32 we try to detect and read a keystroke within a given timeout period (0.6s). Next, in lines 34-45 we discriminate between five keys corresponding to the five commands, plus an option that detects the Ctrl+C key combination (the \x03 ASCII character) to stop the node execution. For instance, if an i
is detected, the command message will be forward
. If a ,
is detected, the message will be backward
, and so on. With lines 47-49 the current command will be printed to the terminal window only if it’s different from the last detected one, to avoid cluttering the window. Line 50 publishes the current command as a ROS message to the /command
topic, and line 51 puts the node to sleep for a period of approximately 0.1s (the node’s execution rate is 10Hz). When the node is woken up, the infinite loop repeats again.
I copied the getKey(key_timeout)
function defined in lines 53-61 from the widely known teleop_twist_keyboard
ROS node used to manually control a ROS robot from keyboard inputs. How this function works is not really important. Basically, it allows us to read a key from the keyboard without needing to hit “Enter.” Nothing directly related to ROS, really. It’s just that this was the easiest way I found to demonstrate a basic example, using only the computer resources without additional hardware. With lines 63-68 we run talker()
as the node’s main entry function. Line 64 is just an initialization step to get the getkey()
function working.
THE LISTENER NODE
Listing 3 shows the code for the drive_node.py
listener node. Lines 2-6 play similar roles as in the previous node. Lines 8-11 define the listener()
function containing the node’s initialization steps. With line 9, we initialize the node with the name motor_driver
. Node names must be unique, there can’t be two nodes with the same name. With line 10, we subscribe to the /command topic, from which we will be receiving command ROS messages of type String from the other node. The callback function for the subscriber is commandCallback
(more on this later). Line 11 puts the node into an infinite loop until it is shut down. The difference with rospy.sleep()
used in the previous node is that rospy.spin()
makes the node execute only when messages in the topic arrive. How to pick one or the other really depends on the application. If a node needs to execute only when certain events occur, you can use rospy.spin()
, but if a node needs to do processing at regular intervals (for instance, to read from sensors), you can use rospy.sleep()
instead.
When you subscribe to a topic, typically you define a callback function that will execute as a message handler for the subscribed topic. Lines 13-34 define the callback function for this node’s subscriber. With lines 14-15 we state our intention to locally access the global variables defined in lines 5-6. With line 17, we retrieve the command inside the received message data, and in lines 19-30 we discriminate among five possible commands to run a corresponding function that will control our fictional robotic car.
For instance, if a forward
message is received, the forward()
function will be executed, driving the motors in a way that will make the robotic car go forward, and so on. Lines 32-34 print the last received command only if it is different from the previous one, to avoid cluttering the terminal window. Lines 36-54 define the five driving functions for the robotic car, but because we are interacting with a fictional one, the functions are empty. In Part 2 of this article, we will implement those functions to control a real robotic car! Finally, in lines 56-60, we launch the listener()
function as the node’s main entry function, and we stop the motors if the node is shut down (line 60).
CONCLUSION
I hope the material presented here gives you a fair picture of what ROS is and how it works. The example we just built illustrates a basic workflow with ROS. Put more nodes with specific code, chain them properly in the computation graph by advertising and subscribing to certain topics to exchange data, integrate other third-party nodes, and you have the basic recipe for a more sophisticated robotic application!
If you are learning ROS for the first time, it will be much more productive if you try the presented example for yourself. After all, you just need a personal computer with Ubuntu and some free time. If you just have a Windows or MacOS computer, you can create an Ubuntu virtual machine with VirtualBox or other virtualization software. Although it is technically possible to install ROS directly on Windows 10 or MacOS, Ubuntu seems to be the easiest way for beginners. I use ROS with both real and virtual Ubuntu machines all the time without issues; however, a real Ubuntu machine is preferable to a virtual one, if you want to avoid the extra hassle of learning how to configure and use VirtualBox properly.
In Part 2 (Circuit Cellar 369, April 2021) will explain how to build a robotic car based on Raspberry Pi that uses ROS, with the perspective of being a low-cost platform for learning and experimenting further with ROS. Stay tuned!
RESOURCES
References:
[1] ROS Introduction https://wiki.ros.org/ROS/Introduction
[2] ROS Concepts http://wiki.ros.org/ROS/Concepts
[3] ROS Client Libraries, http://wiki.ros.org/Client%20Libraries
[4] Ubuntu install of ROS Noetic, http://wiki.ros.org/noetic/Installation/Ubuntu
ROS Wiki | https://wiki.ros.org
PUBLISHED IN CIRCUIT CELLAR MAGAZINE • MARCH 2021 #368 – Get a PDF of the issue
Sponsor this ArticleRaul Alvarez Torrico has a B.E. in electronics engineering and is the founder of TecBolivia, a company offering services in physical computing and educational robotics in Bolivia. In his spare time, he likes to experiment with wireless sensor networks, robotics and artificial intelligence. He also publishes articles and video tutorials about embedded systems and programming in his native language (Spanish), at his company’s web site www.TecBolivia.com. You may contact him at raul@tecbolivia.com