Skip to content

2. ROS Tutorial

Joseph Saliba edited this page Oct 17, 2023 · 15 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. 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

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.

Exercices

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 use parameter -1 in rostopic pub -1 …, read more about it here (1.8)]

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.

Exercise III

Prerequisite

  • Have completed the (Work Environment Setup)[https://github.com/mcgill-robotics/rover/wiki/1.-Work-Environment-Setup]
  • Install pynput (Python module for keyboard interfacing):
pip install pynput
  • Install pyqt5
pip install PyQt5
  • Install turtlesim (Swimming turtle sim):
sudo apt-get install ros-noetic-turtlesim

Steps to complete Exercise

  1. Complete prerequisites above
  2. Start a clean ROS master (roscore).
  3. List the currently active ROS topics.
  4. Run rosrun turtlesim turtlesim_node. A window with a turtle should have popped up. That's turtlesim!. turtlesim is a program that is running on ROS. The objective of this exercise is for you to write scripts which interact with turtlesim through ROS topics.
  5. List the currently active ROS topics, You should notice 3 new topics. For now, focus on the topic /turtle1/cmd_vel.
  6. Run the command rostopic info turtle1/cmd_vel for you to see the message type of the topic.
  7. Run a command which publishes to the turtle1/cmd_vel topic. You will need to know the type of the topic from step 6. Pressing TAB on your keyboard is very helpful for autocomplete here. If the command was successful, you should see the turtle move.
  8. Now, Modify the controls.py python file (see below), following all the # TODO comments, to develop the control interface for the turtle. The goal is to move the turtle by pressing the arrow keys on your keyboard. The implementation of the keys input has already been implemented for your convenience. Run the python file and press on arrow keys to test the script. When an arrow key is pressed, a ROS message should be sent to the turtle1/cmd_vel command, similar to step 7.
  9. We are now interested in retrieving information from turtlesim, particularly, the x and y position of the turtle, as well as its linear velocity. Fortunately, turtlesim publishes this information to a topic called /turtle1/pose. In the console, run the command which echoes the messages received of this topic.
  10. Since reading from console is often not practical, we would like to display these on a basic user interface. Modify the ui.py (see below) application, following the # TODO markings, creating a subscriber which updates the labels on the UI whenever turtlesim publishes to /turtle1/pose. While running controls.py in a terminal, open a new one and run the UI. Test to see if moving the turtle changes the labels on the UI.

Controls.py

import rospy
from pynput import keyboard
#  TODO: import the necessary message types: `from geometry_msgs.msg import ...`

def on_arrow_key(pub: rospy.Publisher, key):
    """
    This function uses the pynput.keyboard.Listener to listen for arrow key presses.
    Depending on the value of `key`, it publishes a message to the passed publisher `pub`.

    To figure out the type of the message to publish, run `rostopic info /turtle1/cmd_vel`.

    TODO: Replace `pass` statements with your code.
    """
    if isinstance(key, keyboard.Key):
        if key == keyboard.Key.up:
            # move forward
            pass
        elif key == keyboard.Key.down:
            # move backward
            pass
        elif key == keyboard.Key.left:
            # turn left
            pass
        elif key == keyboard.Key.right:
            # turn right
            pass


if __name__ == '__main__':
    # TODO: Initialize a ros node. Name it 'turtle_control_node'.

    controls_p = None  # TODO: Replace `None` to create a publisher for the turtle's velocity:
                       # controls_p = rospy.Publisher('/turtle1/cmd_vel', ..., queue_size=10)
    
    with keyboard.Listener(on_press=lambda key: on_arrow_key(controls_p, key)) as listener:
        listener.join()
    rospy.spin()

ui.py

import sys
import rospy
from turtlesim.msg import Pose
from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QLabel

class ROSDataSubscriber(QWidget):
    def __init__(self):
        super(ROSDataSubscriber, self).__init__()

        self.init_ros()
        self.init_ui()

    def init_ros(self):
        rospy.init_node('ros_data_subscriber', anonymous=True)
        self.subscriber = None  # TODO: Replace `None` with a subscriber to the topic '/turtle1/pose' with the message type `Pose`.
                                #  The subscriber calls a callback function that updates the text of the 3 labels: x position, y position, and linear velocity.

    def update_values(self, data: Pose):
        """
        `data` is the message received from the subscriber.

        TODO: Replace `pass` with your code.
        Update the text of the 3 labels: x position, y position, and linear velocity.
        ie: self.turtle_x_pos.setText('...')
        """
        pass  


    def init_ui(self):
        self.setWindowTitle('ROS Data Subscriber')
        self.setGeometry(100, 100, 500, 300)

        layout = QVBoxLayout()
        self.turtle_x_pos = QLabel('Waiting for data...')
        self.turtle_y_pos = QLabel('Waiting for data...')
        self.turtle_lin_vel = QLabel('Waiting for data...')
        layout.addWidget(self.turtle_x_pos)
        layout.addWidget(self.turtle_y_pos)
        layout.addWidget(self.turtle_lin_vel)

        self.setLayout(layout)

def main():
    app = QApplication(sys.argv)
    window = ROSDataSubscriber()
    window.show()
    sys.exit(app.exec_())

if __name__ == '__main__':
    main()

Clone this wiki locally