ROS Basics

The goal of this tutorial is to set up a workspace for all the resources required to install and develop as part of our journey to control robots. We will work with ROS (the most widely used open middleware for robotics) on a Linux-Ubuntu computer, which is the best-supported operating system for ROS.

This tutorial assumes you are using an Ubuntu-like 22.04 computer with configured Python and C++ APIs. It is based on ROS2 Iron.

The wiki.ros.org website primarily refers to ROS1. While ROS1 and ROS2 share similarities, there are significant differences between them. Be cautious when interpreting search results on the internet, as they might refer to ROS1 instead of ROS2.

Nodes and Topics

Robots are complex cybernetic systems that require complex software to handle complex missions. That for, the main feature proposed by ROS is to develop programs in a modular way. Pieces of programs are dedicated to atomic functionalities and the overall control program is composed of several interconnected pieces.

A piece of program is called node and nodes exchange data by communicating through topics.

This tutorial part relies directly on ROS documentation docs.ros.org.

  1. Configuring environment: You should well understand how to configure your Linux environment
  2. First contact: Then, you can play with the hello world example on ROS2. Notice that sudo commands suppose you have administrator privilege on the computer
  3. Understanding nodes
  4. Understanding topics

At this point you should be comfortable with the core component of ROS, nodes and topics. You realized that you will interact a lot with your software architecture throuh your terminal. So a good practice consists of turning your terminal prompt to clearly state your ROS configuration at any time...

Add the following 2 lines into your ~/.bashrc file. The bash run command file (~/.bashrc) is a good tool to configure all your terminal with common environment variables and configurations.

# Tunned prompt:
PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\h\[\033[00m\]:($ROS_AUTOMATIC_DISCOVERY_RANGE::$ROS_DOMAIN_ID)\[\033[01;34m\]\w\[\033[00m\].\n\$ '

Python API

ROS include more elements, components like services and actions, features like parameters or tools to debug (rqt_console), visualize (rviz2), launch configurations of nodes (launch) or recording topics (bag). But let see the Python API (Application Programming Interface) on nodes and topics first, before to visit other ROS elements.

Notice that the purpose of this lecture and its tutorials is not to cover all ROS elements. Furthermore, the tutorials are designed with ament_cmake build tool, mostly python node and yaml launch files. Other solutions are provided by ROS2.

Setup a Dev. Environment

First, you should work on a dedicated directory to regroup the ROS things you need and you develop, let say your ros_space or ros_ws. The things would generally be packages but we will see that later. In this workspace, we will work for now on a playground directory and mode specifically on ROS talker and listener python script.

To set up our environment in a terminal:

cd                               # Go to the home directory of the current user.
mkdir ros_space                  # Create a directory
cd ros_space                     # Enter it
mkdir playground                 # Create a directory
touch playground/talker.py       # Create a file (if it does not exist)
touch playground/listener.py     # Create a file (if it does not exist)

We recommend working with a complete extensible IDE, typically Visual Studio Code. And open the entire ros_space as a project, plus a terminal inside the IDE. At this point, your IDE should have \(3\) areas: the explorer on the left side, a text editor on top of a terminal. The explorer should contain the playground directory with your \(2\) python scripts inside.

Talker: a simple topic publisher

The goal of our first program is to send a message on a topic. For that, we need the ROS client Python package (rclpy) for initializing ROS and creating nodes, and all the packages defining all the types of messages the program should manipulate (only simple string message on our example).

At the end the first hello world program will look like:

import rclpy
from rclpy.node import Node
from std_msgs.msg import String

def oneTalk():
    # Initialize ROS client
    rclpy.init()
    # Create a node
    aNode= Node( "simpleTalker" )
    # Attach a publisher to the node, with a specific type, the name of the topic, a history depth
    aPublisher= aNode.create_publisher( String, 'testTopic', 10 )
    # Create a message to send
    msg = String()
    msg.data = 'Hello World'
    # Add the message to the list of messages to publish
    aPublisher.publish(msg)
    # Activate the ROS client with the node
    # (that will publish the message on testTopic topic)
    rclpy.spin_once(aNode, timeout_sec= 10.0)
    # Clean everything and switch the light off
    aNode.destroy_node()
    rclpy.shutdown()

# Execute the function.
if __name__ == "__main__":
    oneTalk()

Run the solution on a first terminal.

python3 playground/talker.py

Listen to the topic in another terminal (you have to run again the talker to see a message...).

ros2 topic echo testTopic

The talker node need to be active when echo testTopic is started otherwise ros2 topic end because no node is connected to testTopic.

Also, the String message description (ie. composed by a unique data attribute) come from index.ros.org, aside from the other standard message type of std_msgs.

The main ROS packages defining message types are grouped on common_interfaces with, for example geometry_msgs for objects defined on the Cartesian coordinate system, visualization_msgs focused on message types for visualization or sensor_msgs, well, for sensor-based data.

Continuous Talker

In a second version, we aim to create a node publishing continuously messages. For that purpose, we use an Event-Driven Programming style.

Technically in ROS, it is managed by the ros client, with the spin_once or spin functions. These \(2\) functions connect events (the reception of a message for instance) with the appropriate callback.

spin_once activates once the ros client and wait for the next event. It should be called in an infinite loop. spin handle the infinite loop directly (and should be preferred).

For the talker, you have to generate events with timers:

aNode.create_timer( duration, aCallback )

The call back function aCallBack will be called after a duration is terminated.

Furthermore, the call-back functions generally require a context of execution. Some already defined and accessible elements (for instance in our case, the publisher). For that purpose, we implement our aCallBack as a method of a class.

The new talker script will lookalike:

def infiniteTalk():
    # Initialize ROS node with ROS client
    rclpy.init()
    aNode= Node( "infTalker" )
    talker= ROSTalker(aNode)
    # Start infinite loop
    rclpy.spin(aNode)
    # Clean everything and switch the light off
    aNode.destroy_node()
    rclpy.shutdown()

class ROSTalker:
    def __init__(self, rosNode):
        self._publisher= rosNode.create_publisher( String, 'testTopic', 10 )
        self._timer = rosNode.create_timer(0.5, self.timer_callback)
        self._i = 0

    def timer_callback(self):
        msg = String()
        msg.data = 'Hello World: %d' % self._i
        self._publisher.publish(msg)
        self._i += 1

# Execute the function.
if __name__ == "__main__":
    infiniteTalk()

Now python3 playground/talker.py wil start a ROS node publishing \(2\) hello messages per second. A Ctr-C in the terminal should stop the process.

Listener: a simple topic subcriber

In a very similar way, listener node is implemented with topics subscription mechanisms associated with a callback.

import rclpy
from rclpy.node import Node
from std_msgs.msg import String

def listen():
    # Initialize ROS node with ROS client
    rclpy.init()
    aNode= Node( "listener" )
    listener= ROSListener(aNode)
    # Start infinite loop
    rclpy.spin(aNode)
    # Clean everything and switch the light off
    aNode.destroy_node()
    rclpy.shutdown()

class ROSListener():

    def __init__(self, rosNode):
        self._logger= rosNode.get_logger()
        self._subscription= rosNode.create_subscription(
            String, 'testTopic',
            self.listener_callback, 10
        )

    def listener_callback(self, msg):
        self._logger.info( 'I heard: ' + msg.data)

if __name__ == '__main__':
    listen()

Run your listener as you run your talker. The \(2\) nodes work together and define a very simple first software architecture.

Logs

Notice that ROS provides its own, "print" function with a logger instance. The logger, attached to a node instance (aRosNode.get_logger()), records logs through different channels: info, debug, warning or error. More on docs.ros.org.

Logs can be followed with rqt_console tool. Follow the official tutorial to learn how to use this tool.