Skip to content

4.2 ROS Tutorial

Antoine Dangeard edited this page Sep 7, 2023 · 10 revisions

Introduction to ROS

Robot Operating System (ROS) is a framework designed to help people build and control robots. Despite its name, ROS isn't exactly an operating system like Windows or macOS; rather, it's a collection of software tools and libraries. Imagine ROS as the toolbox that robot developers use to build and control their robots. It provides a set of pre-made tools that make it easier to create different components of a robot's functionality. These components can include things like controlling motors, processing sensor data (like cameras and sonars), making decisions, and communicating with other robots or computers. Due to its useful functionalities, many universities and companies use it in their robots as a de facto standard for robot programming.

The main characteristics of ROS can be divided into 5 areas:

  1. Peer-to-peer: Individual programs communicate over defined API (ROS messages, services, etc.).
  2. Distributed: Programs can be run on multiple computers and communicate over the network.
  3. Multi-lingual: ROS modules can be written in any language for which a client library exists (C++ and Python are the two most used).
  4. Lightweight: Stand-alone libraries are wrapped around with a thin ROS layer.
  5. Free and open-source: Most ROS software is open-source and free to use.

ROS Computational Structure

The ROS computation structure is composed of 7 main elements:

  1. Nodes: Nodes are processes that perform computation. ROS is designed to be modular at a fine-grained scale; a robot control system usually comprises many nodes. For example, one node controls a laser range-finder, one controls the wheel motors, another node performs localization, and so on. A ROS node is written with the use of a ROS client library, such as roscpp or rospy (these will be used in this tutorial).
  2. Master: The ROS Master provides name registration and lookup to the rest of the computation structure. Without the Master, nodes would not be able to find each other, exchange messages, or invoke services. Nodes connect to other nodes directly; the Master only provides lookup information.
  3. Parameter Server: The Parameter Server allows data to be stored by key in a central location. It is part of the Master.
  4. Messages: Nodes communicate with each other by passing messages. A message is simply a data structure, comprising typed fields. Standard primitive types (integer, floating point, boolean, etc.) are supported, as are arrays of primitive types. Messages can include arbitrarily nested structures and arrays (much like C structs).
  5. Topics: Messages are routed via a transport system with publish/subscribe semantics. A node sends out a message by publishing it to a given topic. The topic is a name that is used to identify the content of the message. A node that is interested in a certain kind of data will subscribe to the appropriate topic. There may be multiple concurrent publishers and subscribers for a single topic, and a single node may publish and/or subscribe to multiple topics. In general, publishers and subscribers are not aware of each others' existence. The idea is to decouple the production of information from its consumption. Logically, one can think of a topic as a strongly typed message bus. Each bus has a name, and anyone can connect to the bus to send or receive messages as long as they are the right type.
  6. Services: The publish/subscribe model is a very flexible communication paradigm, but its many-to-many, one-way transport is not appropriate for request/reply interactions, which are often required in a distributed system. Request/reply is done via services, which are defined by a pair of message structures: one for the request and one for the reply. A providing node offers a service under a name and a client uses the service by sending the request message and awaiting the reply. ROS client libraries generally present this interaction to the programmer as if it were a remote procedure call.
  7. Bags: Bags are a format for saving and playing back ROS message data. Bags are an important mechanism for storing data, such as sensor data, that can be difficult to collect but is necessary for developing and testing algorithms.

Publishing to a topic

There are two ways to publish to a topic: from the command line or from an executable file (using rospy, for python, and roscpp, for C++).

Command Line

In this example, we will publish the string type message “Hello” to the topic hello. In order to check whether it worked or not, we need to first use a command that echoes every message received in a given topic. First, we need to source the environment variables from the setup.bash file.

  $ cd AUV-2024/catkin_ws
  $ source devel/setup.bash

As mentioned in section 2, we can only run nodes if there is a master node running. So, before creating the node that publishes to a topic, we will create the master node.

 $ roscore

1

The roscore command is responsible for starting up a ROS master, ROS parameter server, and rosout logging node.

In a new tab (click on the top-left button), we can use the echo command. But first, remember to always source the setup.bash file when you open a new tab. You must do this because each tab is a new bash process that has its own environment variables.

  $ rostopic echo /hello

For more information on rostopic commands, you can check out here. Topics are named using a slash before the topic name - /topic_name.

In a new tab, let’s publish the message “Hello”.

  $ rostopic pub /hello std_msgs/String Hello

The structure of publishing a message is very intuitive: rostopic pub /topic_name topic_type content_to_publish. If you go back to the tab with the echo command, you will see the message “Hello” published.

2

→ EXERCISE I: Your first exercise is to publish from the command line three messages “1”, “3”, and “5” as integers to a topic called odds (for more std_msgs types, click here). [HINT: if you want to publish three different messages on the same tab, you should the parameter rostopic pub -1 …, read more about it here (1.8)]

Executable File - Python (rospy)

This next publisher will infinitely publish the integer 1. Again, you will need to have one terminal running roscore. Then, open your preferred code editor and create a python file. Its location does not matter as long as it is within catkin_ws.

#!/usr/bin/env python
import rospy
from std_msgs.msg import Int16

def publisher():    
    while not rospy.is_shutdown():
        pub.publish(Int16(1))
 
if __name__ == '__main__':
    rospy.init_node('publisher')
    pub = rospy.Publisher('odds', Int16, queue_size=1)
    publisher()

The first part of any python node is to make sure the code is run using python. For those of you who have taken COMP 206, this will seem familiar. In the python file, your first line should always be:

#!/usr/bin/env python

Then we need to import rospy, which is a pure Python client library for ROS, as well as the type of the message we will use.

import rospy
from std_msgs.msg import Int16

Now that everything is set up, we can write a function to publish our message. Note that you don't have to use a function to publish a message. However, it is more difficult to read and fix the code as the code becomes more complex. So, it is good practice to keep it well structured.

    rospy.init_node('publisher')

rospy.init_node(node_name, ...), is very important as it initializes the node and tells rospy the its name -- until rospy has this information, it cannot start communicating with the ROS Master.

    pub = rospy.Publisher('odds', Int16, queue_size=1)

This line declares that your node is publishing to the 'odds' topic using the message type Int16. The structure of the Publisher function is rospy.Publisher(topic_name, msg_type, queue_size). Queue_size is the size of the outgoing message queue used for asynchronous publishing. For more information on what that actually means and how you should choose a good value, read this (1.3 - 1.4).

    while not rospy.is_shutdown():
        pub.publish(Int16(1))

This part will keep publishing the value 1 of type Int16 until you shutdown the execution. Make sure to use the actual type you import instead of assuming Python's standard types are the same (e.g., if you try to publish just 1 instead of Int16(1), you will get an error -- the format of how the integer is represented is different -- so always convert the message to the desired type).

Note: you should echo the topic after running the file to see the output.

Subscribing to a topic

Different from publishing, you can only subscribe to a topic from an executable file (rospy or roscpp).

Executable File - Python (rospy)

In this example, we will perform the following steps:

  1. Create a subscriber that subscribes to the topic we created for the publisher
  2. Perform manipulations with the messages received
#!/usr/bin/env python
import rospy
from std_msgs.msg import Int16

def sub_cb(msg):
    print(msg.data + 1)
 
if __name__ == '__main__':
    rospy.init_node('subscriber')
    sub = rospy.Subscriber('odds', Int16, sub_cb)
    rospy.spin()

Some of the lines are the same in both examples, so I'll skip those. Let's focus on the differences.

    sub = rospy.Subscriber('odds', Int16, sub_cb)

Instead of creating a publisher, we create a subscriber using the format rospy.Subscriber('topic_name', topic_type, callback_function). The main difference is the callback (cb) function. When new messages are received from the subscribed topic, the callback function is invoked with the message as the first argument.

There is one thing I must clarify. When I said a message has a type, you should think of the type as a class. These types have attributes that hold values or more attributes. The more complex a type is (e.g., sensors), the more attributes it will have. To access these attributes, you need to use the "dot" notation (e.g., if the name of the attribute is time and the name of the message is msg, then you can write msg.time to access the value in that attribute). For those of you familiar with C++, it is the same format as structs. In this example, the type Int16 only has one attribute called data.

When the value is retrieved, you can perform any operations you want with it.

    rospy.spin()

The final addition, rospy.spin() simply keeps your node from exiting until the node has been shutdown.

→ Exercise II: Your second exercise should be to create two string publisher and a string subscriber. The first publisher should publish "My name is " until shutdown. The subscriber should subscribe to the same topic and add your first name to the string. Then, you should take the string with your first name and publish to a different topic (name it as you like) using the second publisher until shutdown.

Solutions

→ Exercise I: You should have 3 terminals that look like the followings 1 3 4

→ Exercise II: There are different ways to do it. If you were able to do it, then consider my answer as an alternative way.

File 1:

#!/usr/bin/env python
import rospy
from std_msgs.msg import String

def publisher():    
    while not rospy.is_shutdown():
        pub.publish(String("My name is "))
 
if __name__ == '__main__':
    rospy.init_node('node_one')
    pub = rospy.Publisher('incomplete', String, queue_size=1)
    publisher()

File 2:

#!/usr/bin/env python
import rospy
from std_msgs.msg import String

def sub_cb(msg):
    sentence = msg.data + "[your name here]"
    pub.publish(String(sentence))
 
if __name__ == '__main__':
    rospy.init_node('node_two')
    pub = rospy.Publisher('complete', String, queue_size=1)
    sub = rospy.Subscriber('incomplete', String, sub_cb)
    rospy.spin()

Remember to run file 1 before running file 2. To see the result, open a new tab and use the rostopic echo /topic_name command.

Clone this wiki locally