ROS2 Launch Files

ROS2 allows you to run individual nodes with the command:

$ ros2 run <package_name> <node_name>

This is nice and fun if you are just running a couple of nodes at the same time, but imagine you need to run 10-20 nodes like this. That would be really cumbersome to do so. Instead, you can use a so-called launch file. These files allow you to run multiple nodes with a single command.

$ ros2 launch <package_name> <launch_file_name.launch.py>

Launch files have another advantage: you can also set different ROS2 parameters that are taken over to the individual nodes, and you can also specify to change topic names. Another nice feature is that you can group multiple nodes into a logical group, especially when these nodes are always started together. For example, if you have a service server that subscribes to a camera topic and saves the image when being called, you want to start the camera node and the service server always together. n this case, a launch file will do the trick.

Create a launch file for a Python package

You have already seen how to start a ROS program by using the ros2 run command. The ros2 run command allows you to start one single program at a time. In most cases, one single program will not be enough to get your robot up and running. In these cases, a launch file will make your life easier.

First, create a directory called launch to organize your package. Therefore, you must be inside your my_turtlesim folder.

$ cd ~/ros2_ws/src/my_turtlesim/
$ mkdir launch

Now, enter the new directory to create a new file. You can do this with your file browser or with the terminal:

$ cd launch

In ROS2, there are 3 different types of launch files: Python files, XML files and YAML files. In ROS1, there were only XML files. Since Python files provide the most flexibility, they are often seen as the default type of launch files in ROS2.

You can create a new Python launch file and open it with your terminal with the following command:

$ gedit my_turtlesim.launch.py

Now, a new window should open up. It shows an empty text file. The launch.py extension is used as a convention to show that it is a launch file and not simply a Python script.

Type the following code into the launch file and save the file:

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='turtlesim',
            executable='turtlesim_node',
            name='my_turtlesim_node',
            output='screen',
        ),
       Node(
            package='my_turtlesim',
            executable='my_simple_publisher',
            name='my_simple_publisher',
            output='screen',
        )
    ])

You can now save the file and close it.

The first lines import the necessary modules to launch ROS2 nodes.

from launch import LaunchDescription
from launch_ros.actions import Node

Next, a launch description is defined, including all the nodes that you want to launch. In this example, we would like to launch the turtlesim node inside the package turtlesim. The name of the executable is turtlesim_node. This is also the name that you would use when using ros2 run. The name in this case can be used to overwrite the actual node name as defined inside the node. This can be interesting if you launch the same node multiple times and you want to avoid collisions caused by two nodes with the exact same name. The output will define where the ROS2 log entries, which usually appear in the terminal, will be sent to. A launch file will default simply write them in a log file and not display anything on the screen. Here, we actually tell ROS2 to write these log entries on the screen.

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='turtlesim',
            executable='turtlesim_node',
            name='my_turtlesim_node',
            output='screen',
        ),

Inside the launch description, you can also define a second node you want to call. In this case, you can add the simple publisher that makes the turtle move in a circle. This was a package that you created by yourself. The syntax is exactly the same. You indicate the package name, the executable name and the node name.

       Node(
            package='my_turtlesim',
            executable='my_simple_publisher',
            name='my_simple_publisher',
            output='screen',
        )
    ])

Before you can use the new launch file, you need to modify the package.xml file and the setup.py file. Open the package.xml file:

$ gedit ~/ros2_ws/src/my_turtlesim/package.xml

And add the following line to the file:

<exec_depend>ros2launch</exec_depend>

Save the file and close it. Now, open the setup.py file:

$ gedit ~/ros2_ws/src/my_turtlesim/setup.py

And modify the data_files part by adding the following entry:

(os.path.join('share', package_name, 'launch'), glob('launch/*.py')),

You also need to add some imports at the beginning of the file:

from glob import glob
import os

The file should now look as follows:

from setuptools import setup
from glob import glob
import os

package_name = 'my_turtlesim'

setup(
    name=package_name,
    version='0.0.0',
    packages=[package_name],
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
        (os.path.join('share', package_name, 'launch'), glob('launch/*.py')),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='dave',
    maintainer_email='dave@todo.todo',
    description='TODO: Package description',
    license='TODO: License declaration',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'my_first_node = my_turtlesim.my_first_program:main',
            'my_simple_publisher = my_turtlesim.my_simple_publisher:main',
            'my_simple_subscriber = my_turtlesim.my_simple_subscriber:main',
            'my_service_server = my_turtlesim.my_service_server:main',
            'my_service_client = my_turtlesim.my_service_client:main',
        ],
    },
)

Save the file and close it. With these modifications, we tell the ROS2 package to look for launch files and where to look for them. Now, re-build your workspace and source it again:

$ cd ~/ros2_ws/
$ colcon build --symlink-install
$ source install/local_setup.bash

Now, you can launch your new launch file with the following command:

$ ros2 launch my_turtlesim my_turtlesim.launch.py

As you can see, this command is now starting the turtlesim node and at the same time, it makes the turtle move in a circle. This means, two ROS2 nodes have been started with a single command.

Other Launch File Formats

As mentioned before, there are three types of launch files: Python, XML and YAML. ROS1 programmers will be most familiar with XML launch files. You can create one by opening a new text file:

$ gedit ~/ros2_ws/src/my_turtlesim/launch/my_turtlesim.launch.xml

Now, insert the following code:

<launch>  
	<node pkg="turtlesim" exec="turtlesim_node" output="screen" />  
	<node pkg="my_turtlesim" exec="my_simple_publisher" output="screen" />  
</launch>

Save the file and close it. As you can see, this code is quite different but also much shorter. At the same time, since XML is not a scripting language, it provides less flexibility and functionality than the Python version.

In some cases, it can be interesting to write down the entries in multiple lines, especially when more elements are added to the file:

<launch>  
	<node pkg="turtlesim" 
          exec="turtlesim_node" 
          output="screen" />  
	<node pkg="my_turtlesim" 
          exec="my_simple_publisher" 
          output="screen" />  
</launch>

This can add some clarity as all the elements are in a block.

Let’s also create a YAML launch file:

$ gedit ~/ros2_ws/src/my_turtlesim/launch/my_turtlesim.launch.yaml

Add the following code:

launch:
    - node: {pkg: "turtlesim", exec: "turtlesim_node", output: "screen"}  
    - node: {pkg: "my_turtlesim", exec: "my_simple_publisher", output: "screen"} 

Save the document and close it.

As you can see, the YAML file is even shorter than the XML file. The YAML file has a bit more of a symbol-based syntax where you need to pay attention to put all the brackets, colons and commas in the right position. XML uses tags that make the file more verbose but also more explicit.

Now, you also need to modify the setup.py file again:

$ gedit ~/ros2_ws/src/my_turtlesim/setup.py

Just as with the Python files, you need to tell ROS2 where to look for the launch files. This includes the file extensions .launch.xml and .launch.yaml.

        (os.path.join('share', package_name, 'launch'), glob('launch/*.xml')),
        (os.path.join('share', package_name, 'launch'), glob('launch/*.yaml')),

The entire file should then look like this:

from setuptools import setup
from glob import glob
import os

package_name = 'my_turtlesim'

setup(
    name=package_name,
    version='0.0.0',
    packages=[package_name],
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
        (os.path.join('share', package_name, 'launch'), glob('launch/*.py')),
        (os.path.join('share', package_name, 'launch'), glob('launch/*.xml')),
        (os.path.join('share', package_name, 'launch'), glob('launch/*.yaml')),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='dave',
    maintainer_email='dave@todo.todo',
    description='TODO: Package description',
    license='TODO: License declaration',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'my_first_node = my_turtlesim.my_first_program:main',
            'my_simple_publisher = my_turtlesim.my_simple_publisher:main',
            'my_simple_subscriber = my_turtlesim.my_simple_subscriber:main',
            'my_service_server = my_turtlesim.my_service_server:main',
            'my_service_client = my_turtlesim.my_service_client:main',
        ],
    },
)

Save the file and close it. Head back to the root directory of the workspace, build the package and source the workspace again:

$ cd ~/ros2_ws/
$ colcon build --symlink-install
$ source install/local_setup.bash

You can launch the XML launch file with the following command:

ros2 launch my_turtlesim my_turtlesim.launch.xml

And the YAML file with the following command:

ros2 launch my_turtlesim my_turtlesim.launch.yaml

The XML and the YAML files will start the turtlesim node and make it move in a circle, just like the Python version.

ROS2 Services

Services in ROS2 are another type of communication between different ROS2 nodes. After you learned how to use Topics, you might wonder, why you would need something else for communication, right?

As explained in the overview, different communication types are used for different purposes. While Topics are mainly used for continuous streams of data, Services will be used for tasks where a constant data stream would be overkill, especially when the data exchange is less frequent. Another use case would be when your program relies on receiving feedback from a sent information, for example when your robot finished a task.

You can find more information about Services in the official ROS2 documentation.

What are Services?

A Service is a type of communication that adopts the idea of a handshake protocol as it is implemented by having a client application that will send a request to the server to perform a task. After the server has finished the task, it will send a message back to the client to notify it of the result. The client will patiently wait for a response before it will continue with other tasks. Especially this last behaviour is one of the major drawbacks of using services as the client is basically unresponsive while waiting for the response. The advantage is that it provides a simple and elegant solution that receives a response rather than broadcasting into the unknown while producing a massive amount of unnecessary data traffic.

If you would like to reproduce the same strategy with Topics, you would need to have two publishers and two subscribers that constantly send the request and the answer to make sure the message has been sent and the result has been received. This would create unnecessary data traffic to obtain the same result.

After all these explanations, let’s have a look at a service message. As already mentioned, there is two-way communication which means the messages have two components. An example is the service message /turtle1/set_pen which is a message for the TurtleSim program seen earlier.

Launch the turtlesim node with:

$ ros2 run turtlesim turtlesim_node

Now, you can have a look at the services that are available with the following command:

$ ros2 service list

The following list will appear:

ros2 service list with the turtlesim application running

We can see there is a service called /turtle1/set_pen. We can have a closer look at this service with the following command:

$ ros2 service type /turtle1/set_pen

This command will return the message type for this service. In this case, the service type is turtlesim/srv/SetPen.

/turtle1/set_pen message type

To get more information about this service message type, use the interface command:

$ ros2 interface show turtlesim/srv/SetPen

This command will show you the service message description.

turtlesim/srv/SetPen interface description

For the /turtle1/set_pen message, this message looks as follows:

uint8 r
uint8 g
uint8 b
uint8 width
uint8 off
---

In general, the service message has a request and a response part, with their respective data types (which can be any type such as int8, uint16, string, bool, …), that are separated by three dashes as seen below:

requestType request
---
responseType response

Note: The request can be composed of several components or it can be empty, like in the SetPen interface. The same holds true for the response. In the above example, the values of the three base colours are given as part of the request while the response is an empty message. This means that there is a response, but it does not contain any data. Pay attention that, even though the response is empty, it needs to be sent as otherwise, the client will keep waiting for a response forever.

The message type of the /turtle1/set_pen message is a /turtlesim/srv/SetPen message, a custom message for this application. There are only a few standard message types for services as they are often very specific for a task. It is fairly common to create a new service message for each service used, unlike topics that usually rely on standard message types.

Using Services in Terminal

In case you only want to get some information about existing Services or you just want to test the function of a service, there is no need to write a ROS2 program to do that. Instead, you can simply use the terminal to do the job.

Starting TurtleSim

If you have no turtlesim node running, you can start it with the following command:

$ ros2 run turtlesim turtlesim_node

The first thing you need to know is, how to find which Services are already available. This allows you to use the Services that already exist.

Finding Information about the Services

As seen above, you can show a list of the existing Services, use the following command:

$ ros2 service list

This command will list all the Services that are already available. The Services available with the turtesim node are the following:

/clear
/kill
/reset
/spawn
/turtle1/set_pen
/turtle1/teleport_absolute
/turtle1/teleport_relative
/turtlesim/describe_parameters
/turtlesim/get_parameter_types
/turtlesim/get_parameters
/turtlesim/list_parameters
/turtlesim/set_parameters
/turtlesim/set_parameters_atomically

Let’s have a closer look at the /spawn service:

$ ros2 service type /spawn

This Service is of the type turtlesim/srv/Spawn. Let’s have a look at its description:

float32 x
float32 y
float32 theta
string name # Optional.  A unique name will be created and returned if this is empty
---
string name

Looks like this Service requires 4 different fields to be called.

Calling the Service

You can call the Service as follows:

$ ros2 service call /spawn turtlesim/srv/Spawn "{x: 2, y: 2, theta: 0, name: 'foxy_turtle'}"

When you execute this command, you will see the following response:

waiting for service to become available...
requester: making request: turtlesim.srv.Spawn_Request(x=2.0, y=2.0, theta=0.0, name='foxy_turtle')

response:
turtlesim.srv.Spawn_Response(name='foxy_turtle')

And you will see that a second turtle has appeared in the turtlesim node:

spawning a new turtle with the spawn service

Moreover, the new turtle has its own set of Services. You can find them with the following command:

$ ros2 service list

The following new Services are available now:

/foxy_turtle/set_pen
/foxy_turtle/teleport_absolute
/foxy_turtle/teleport_relative

These new Services use the name specified in the Service call: /foxy_turtle.

ros2 service list to see new services

In the previous part about Topics, you learned how to Subscribe to Topics and how to Publish them. In this case, though a Service also consists of two parts, you can only use one part in the terminal: the Service Client. The Client sends a request (like you did through the terminal) to a Service Server. The Server performs an algorithm with a return value that is returned back to the Client. As the Server part is usually more complex, it cannot be done in the terminal.

As with the Topics, using the terminal to interact with Services is usually only done for testing or quickly checking if the Services are running.

Using Services in Python

The usual method of using Services is through ROS2 nodes. In the next parts, you will see how to use Services with Python.

As mentioned earlier, there are two parts of the Service: the Client and the Server. These two parts can be split up into two separate Python nodes.

Service Servers

The Server is the part of a Service that is being called and performs an action as a result. When the action is finished, the Server provides a response to the Client that sent the Request.

First, go to the directory containing the Python programs that you made earlier.

cd ~/ros2_ws/src/my_turtlesim/my_turtlesim/

Now, create a new empty file and open it with your text editor.

gedit my_service_server.py

Before you can type the code for running your Service Server node, you want to decide which message type you will use for calling the Service. There are some standard message types that you can use (see here). Let’s say you want to create a service that will allow you to make the turtle move in a circle after the service call. The message will not contain any data except for the information on when to start but you do want to know if the message has been received correctly. Therefore, the Trigger Service message seems ideal as there is no input data and you receive feedback.

Have a look at the Trigger message description:

ros2 interface show std_srvs/srv/Trigger

The Trigger message contains the following information:

---
bool success   # indicate successful run of triggered service
string message # informational, e.g. for error messages

The idea is that the client can send a message to the server to request that the turtle starts moving. As soon as the service call is received the server will reply and make the turtle move in circles.

The code for the Service Server will look as seen here below. Note that the code also contains code to publish to a Topic.

import rclpy
from rclpy.node import Node
from std_srvs.srv import Trigger
from geometry_msgs.msg import Twist


class MyServiceServer(Node):

    def __init__(self):
        super().__init__('my_service_server')
        self.my_service = self.create_service(Trigger, 'draw_circle', self.draw_circle_callback)
        self.publisher_ = self.create_publisher(Twist, 'turtle1/cmd_vel', 10)

    def draw_circle_callback(self, request, response):
        request # request must be specified even if it is not used
        self.get_logger().info('Received request to draw a circle!')
        response.success = True
        response.message = "Starting to draw a circle!"
        timer_period = 0.5  # seconds
        self.timer = self.create_timer(timer_period, self.publish_velocity_callback)
        self.i = 0

        return response

    def publish_velocity_callback(self):
        my_velocity = Twist()
        my_velocity.linear.x = 0.5
        my_velocity.angular.z = 0.5
        self.publisher_.publish(my_velocity)
        self.get_logger().info(f"Publishing velocity: \
            \n\t linear.x: {my_velocity.linear.x}; \
            \n\t linear.z: {my_velocity.linear.x}")
        self.i += 1


def main(args=None):
    rclpy.init(args=args)

    my_service_server = MyServiceServer()

    rclpy.spin(my_service_server)
    rclpy.shutdown()


if __name__ == '__main__':
    main()

In the first part, you only tell ROS2 which modules you import. In this case, you need to import the Service message of the type std_srvs/srv/Trigger and also the geometry_msgs/msg/Twist message for publishing the velocity in a Topic.

import rclpy
from rclpy.node import Node
from std_srvs.srv import Trigger
from geometry_msgs.msg import Twist

Next, you create a class that inherits from the Node class. In its __init__() function, you define the node name, the publisher for the velocity commands and the Service server. The Service takes the message type, the Service name and the callback function as arguments.

class MyServiceServer(Node):

    def __init__(self):
        super().__init__('my_service_server')
        self.my_service = self.create_service(Trigger, 'draw_circle', self.draw_circle_callback)
        self.publisher_ = self.create_publisher(Twist, 'turtle1/cmd_vel', 10)

The moment that a service call is received, it will trigger a callback function in the code. In this case, the function is responsible to make the TurtleSim move in circles. This is done by creating a publisher and then sending messages to the topic /turtle1/cmd_vel. In this implementation, the topic is published repeatedly in a timer callback function. The function finishes by returning the service response. At this point, the turtle starts drawing circles.

    def draw_circle_callback(self, request, response):
        request # request must be specified even if it is not used
        self.get_logger().info('Received request to draw a circle!')
        response.success = True
        response.message = "Starting to draw a circle!"
        timer_period = 0.5  # seconds
        self.timer = self.create_timer(timer_period, self.publish_velocity_callback)
        self.i = 0

        return response

The callback function of the timer simply sets the velocity command to be published and prints the command message to the screen.

    def publish_velocity_callback(self):
        my_velocity = Twist()
        my_velocity.linear.x = 0.5
        my_velocity.angular.z = 0.5
        self.publisher_.publish(my_velocity)
        self.get_logger().info(f"Publishing velocity: \
            \n\t linear.x: {my_velocity.linear.x}; \
            \n\t linear.z: {my_velocity.linear.x}")
        self.i += 1

The main function initialises the ROS2 communication and creates an instance of our MyServiceServer class. Then, it keeps the node alive until CTRL+C is pressed.

def main(args=None):
    rclpy.init(args=args)

    my_service_server = MyServiceServer()

    rclpy.spin(my_service_server)
    rclpy.shutdown()


if __name__ == '__main__':
    main()

Save the code with your text editor and close it. Don’t forget to make your Python program executable:

$ chmod +x my_service_server.py

You need to add the new ROS2 Python script to the setup.py file. Open it with:

$ gedit ~/ros2_ws/src/my_turtlesim/setup.py

Add an entry for your new Python program in the entry_points so it should look as follows:

entry_points={
    'console_scripts': [
        'my_first_node = my_turtlesim.my_first_program:main',
        'my_simple_publisher = my_turtlesim.my_simple_publisher:main',
        'my_simple_subscriber = my_turtlesim.my_simple_subscriber:main',
        'my_service_server = my_turtlesim.my_service_server:main',
    ],
},

Now go to the root directory of your workspace and build the package:

$ cd ~/ros2_ws/
$ colcon build --packages-select my_turtlesim --symlink-install
$ source install/local_setup.bash

The colcon build command should build the package without any issues.

colcon build the my_turtlesim package

If you want to run and test the new service server, you will need to have 3 terminals: one for the turtlesim node, one for the Service Server node and one to call the Service.

In the first terminal, start the turtlesim node:

$ ros2 run turtlesim turtlesim_node

In the second terminal, start the Service Server:

$ ros2 run my_turtlesim my_service_server

In the third terminal, call the Service:

$ ros2 service call /draw_circle std_srvs/srv/Trigger {}

You will see that the turtle will start moving after you called the Service:

turtle making circles after calling the /draw_circle service

After executing the command, you can see how the turtle starts moving in a circle.

Calling the Service from the terminal is useful to test if the Service s working properly. On a real robot, it is more likely that the Service is called by another ROS2 program. This is called a Service Client.

Service Clients

The Client is the part that calls the Service. This means it sends a request to the Server and then waits for a response. One node can consist of several Clients that call different Services.

To start, you can go to the package you already made earlier:

$ cd ~/ros2_ws/src/my_turtlesim/my_turtlesim/

Now you can create a new file called my_service_client.py in which you will write the Python code to create a Service Client node:

$ gedit my_service_client.py

Now, an empty text editor window will pop up where you can type down the following code:

import rclpy
from rclpy.node import Node
from std_srvs.srv import Trigger


class MyServiceClient(Node):

    def __init__(self):
        super().__init__('my_service_client')
        self.my_client = self.create_client(Trigger, 'draw_circle')
        while not self.my_client.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('Waiting for service to become avilable...')
        self.req = Trigger.Request()

    def send_request(self):
        self.future = self.my_client.call_async(self.req)
        rclpy.spin_until_future_complete(self, self.future)
        return self.future.result()


def main(args=None):
    rclpy.init(args=args)

    my_service_client = MyServiceClient()
    response = my_service_client.send_request()
    my_service_client.get_logger().info(
        f'Result for triggering "Draw Circle: \
        \n\tSuccessful: {response.success} \
        \n\tMessage: {response.message}')

    my_service_client.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

What this code does, is first import the rclpy library and the Service message type Trigger from the subfolder srv from the ROS package called std_srvs.

import rclpy
from rclpy.node import Node
from std_srvs.srv import Trigger

The next part creates a class inherited from the Node class. This class, called MyServiceClient initiates the node with the node name my_service_client and creates a Service Client instance using the Service message type and the name of the Service. Next, we create an empty Service request message.

class MyServiceClient(Node):

    def __init__(self):
        super().__init__('my_service_client')
        self.my_client = self.create_client(Trigger, 'draw_circle')
        while not self.my_client.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('Waiting for service to become avilable...')
        self.req = Trigger.Request()

The function send_request() defines a variable called self.future which receives the response from the Service Server. Then, the program spins until the response has been received with the rclpy.spin_until_future_complete() function. In this function, you need to specify which variable contains the received response. The variable name self.future can be chosen freely, but for clarity, it contains the word future to specify it is the response that will be obtained in the near future after making the request. The function ends with returning the received answer.

    def send_request(self):
        self.future = self.my_client.call_async(self.req)
        rclpy.spin_until_future_complete(self, self.future)
        return self.future.result()

The main function initialises the ROS2 communication and then creates an instance of the node class. Then, we added a log entry to display in the terminal that we created a service request for the service /draw_circle. We end the program by destroying the node and shutting down the program.

def main(args=None):
    rclpy.init(args=args)

    my_service_client = MyServiceClient()
    response = my_service_client.send_request()
    my_service_client.get_logger().info(
        f'Result for triggering "Draw Circle: \
        \n\tSuccessful: {response.success} \
        \n\tMessage: {response.message}')

    my_service_client.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

Save the code and then make the file executable with the following command:

$ chmod +x my_service_client.py

Now, add the new node to the setup.py file:

$ gedit ~/ros2_ws/src/my_turtlesim/setup.py

And add a new entry:

entry_points={
    'console_scripts': [
        'my_first_node = my_turtlesim.my_first_program:main',
        'my_simple_publisher = my_turtlesim.my_simple_publisher:main',
        'my_simple_subscriber = my_turtlesim.my_simple_subscriber:main',
        'my_service_server = my_turtlesim.my_service_server:main',
        'my_service_client = my_turtlesim.my_service_client:main',
    ],
},

Save the file and close it. Now go to the root directory of your workspace, build the package and source the workspace:

$ cd ~/ros2_ws/
$ colcon build --packages-select my_turtlesim --symlink-install
$ source install/local_setup.bash

Now, you can test the Service Client. For this, you need 3 terminals: one for the turtlesim node, one for the Service Server node and one to call the Service with your new node.

In the first terminal, start the turtlesim node:

$ ros2 run turtlesim turtlesim_node

In the second terminal, start the Service Server:

$ ros2 run my_turtlesim my_service_server

In the third terminal, run the Service Client:

$ ros2 run my_turtlesim my_service_client

You will see that the turtle will start moving after you run the Service Client node:

run service client

If everything went correctly, the turtle should start moving and a message should appear that the Service call was successful.

Since the program is only spinning until the response has been received, the program will stop automatically. This also means that the program is not responsive until the response has been received. If the server is taking longer or is not responding at all, the client node can be stuck. This could disrupt the behaviour of the robot. Therefore, Services should only be used for tasks where the server will finish quickly such as taking a picture, saving a map or maybe closing or opening a gripper. For tasks that take longer, it is recommended to use ROS2 Actions. Since ROS2 Actions are a bit more advanced, they will not be covered here.

Using Topics

You learned how to use ROS packages to start one or several nodes. You also learned how to create your own ROS programs with Python. In this article, you will learn how to subscribe to a Topic and how to publish to a Topic.

There are many sources covering ROS Topics such as the official ROS wiki (here and here) or other websites like Robotics Back End that you can use for further understanding.

What are Topics?

As already mentioned earlier, a Topic is a way of communication between ROS nodes. This protocol created a data stream from a Publisher to a Subscriber. It is possible that several Publishers are sending data to a Topic at the same time and several Subscribers can listen to a Topic simultaneously.

Each Topic consists of a Topic name and a message type. The name is used to refer to a specific Topic while the message type defines the actual structure of the content. A fairly common Topic name is /cmd_vel which contains a Twist message. Twist messages describe the three velocity parameters for the translation and rotation of a robot. Twist belongs to a category of ROS messages called geometry_msgs. This is simply the ROS package that contains these message definitions. Twist is defined as follows:

Vector3  linear:
    float64 x
    float64 y
    float64 z
Vector3  angular:
    float64 x
    float64 y
    float64 z

Note: You can find more references to the geometry_msgs Twist messages here and here.

This means that you can access the properties of a Twist object in the following way in Python:

my_message = Twist()

my_message.linear.x = 0
my_message.linear.y = 0
my_message.linear.z = 0
my_message.angular.x = 0
my_message.angular.y = 0
my_message.angular.z = 0

First, you define the name of the variable and set it to the variable type of Twist() which is a constructor that creates a Twist object. It initializes all the values to zero.

Topics can also be less complex data types such as Int or String which then only contain a simple integer or string value. These message types belong to the ROS package called std_msgs. Another very common type is sensor_msgs for IMU data, camera data or laser scanner data.

Using Topics in Terminal

In case you only want to see the content of a topic or see what topics are available, you don’t need to write a ROS program to listen to a Topic. You can do this in the terminal as well. You can even publish some data into a Topic. This is mainly used for testing purposes and not really used for actual robot control.

Starting TurtleSim

Before you get started, open a new terminal and start a roscore with the following command:

roscore

Then, open another terminal and start the turtlesim node with the following command:

rosrun turtlesim turtlesim_node

The first thing you need to know is, how to find which Topics are already used by a robot. This is useful as you can use the Topics that are already available rather then creating a new Topic even though, it is not necessary.

Finding Information about the Topics

The following command (again in a new terminal) will show you a list of the Topics that are either being published or subscribed to by a node:

rostopic list

The output in your terminal should look like this:

Now, you know which topics are currently available. You can get more information about these Topics with the following command:

rostopic info /turtle1/pose

This command will provide the following information:

The information you get is that this topic is of type turtlesim/Pose which means it is a message type inside the package called turtlesim. The message type is Pose and it contains the following information:

float32 x
float32 y
float32 theta

float32 linear_velocity
float32 angular_velocity

The Pose messages contain information about the current position and orientation of the turtle and the linear and angular velocity. This block of information is published by the turtlesim regularly.

Subscriber

So the first thing you want to learn is how to see what is inside a Topic. Let’s take /turtle1/pose for example. You can listen to this Topic by using your terminal with the following command:

rostopic echo /turtle1/pose

Now, you will see something like the following:

You can stop the incoming messages by hitting CRTL+c on your keyboard. The Topic that you are looking at is showing you the position of the little turtle on the canvas.

Publisher

Just like you can listen to a Topic through the terminal, you can also write messages to a Topic through the terminal. Therefore, you can use the following command to write to /turtle1/cmd_vel:

rostopic pub /turtle1/cmd_vel geometry_msgs/Twist "linear:
  x: 0.0
  y: 0.0
  z: 0.0
angular:
  x: 0.0
  y: 0.0
  z: 0.5"

Instead of typing the entire command, you can simple start typing the first part of the command and then autocomplete with the TAB key:

rostopic pub /turtle1/cmd_vel [TAB][TAB]

Your terminal will look like this:

And your turtle will rotate on the canvas by 0.5 radians. The terminal will only send your message once and not continuously.

Using the terminal is mainly used for quick verification or testing of a system or for a single event that doesn’t need repetition. For controlling a robot, you will probably write a program that will perform the same tasks autonomously.

Using Topics in Python

When using Python to access Topics, you can have two different kinds of programs: a Subscriber or a Publisher. In addition, you can also have a program that implements several Subscribers or several Publishers or even both.

Subscriber

When making a robotic system, you are more likely to create a ROS node that will take the role of a Subscriber. This has the advantage that you can automatically listen to a Topic and then act depending on the data your program receives.

To start, you can go to the package you already made earlier:

cd ~/catkin_ws/src/my_turtlesim/scripts

Now you can create a new file called simple_subscriber.py in which you will write the Python code to create a Subscriber node:

gedit simple_subscriber.py

Now, an empty text editor window will pop up where you can type down the following code:

#!/usr/bin/env python 

import rospy
from geometry_msgs.msg import Twist

def callback(data):
    print(data)
    print("------------------------------")

if __name__=="__main__":
    rospy.init_node("my_subscriber_node")
    my_subscriber = rospy.Subscriber("/turtle1/cmd_vel", Twist, callback)
    rospy.spin()

Save the code and then make the file executable with the following command:

chmod +x ~/catkin_ws/src/my_turtlesim/scripts/simple_subscriber.py

What this code does, is first importing the rospy library containing the necessary tools to create a ROS node and the Subscriber. Then, you also import the Twist message type that is contained in the ROS package called geometry_msgs in the subfolder msgs.

#!/usr/bin/env python 

import rospy
from geometry_msgs.msg import Twist

The next part will create a function that will be called every time a new message from the Topic is arriving. As you do not call the method by yourself, but it is triggered through the incoming message, this function is often called a callback function. Note: the word callback is not a keyword. so you can give it any name you want. A common practice is to add the word callback inside the function name.

The content of the function is simply printing the received information and then it prints a line to separate the individual messages.

def callback(data):
    print(data)
    print("------------------------------")

Finally, the main program is checking if it is the main program or if this Python script is being imported as a module by another script. Then, it initiates a ROS node. After this, the script creates a Subscriber object that listens to the topic “/turtle1/cmd_vel” which is a message of type Twist and then when receiving a message, it will call the callback function with the incoming message as an argument. Finally, the rospy.spin() function will make sure that ROS doesn’t terminate this script but keeps it active until the user manually stops the script.

if __name__=="__main__":
    rospy.init_node("my_subscriber_node")
    my_subscriber = rospy.Subscriber("/turtle1/cmd_vel", Twist, callback)
    rospy.spin()

You can start the script with the following terminal command:

rosrun my_turtlesim simple_subscriber.py

At first, you will not see anything as the node is not yet receiving any information. This is the case because there is no node publishing to the topic /turtle1/cmd_vel at this moment. You can change this by publishing a message yourself to this topic:

rostopic pub /turtle1/cmd_vel geometry_msgs/Twist "linear:
  x: 0.0
  y: 0.0
  z: 0.0
angular:
  x: 0.0
  y: 0.0
  z: 0.5"

After you start publishing messages to the topic /turtle1/cmd_vel, your simple_subscriber.py will show the following:

The next step is to make a publisher node in Python that will publish Twist messages for you.

Publisher

To start, you can go to the package you already made earlier:

cd ~/catkin_ws/src/my_turtlesim/scripts

Now you can create a new file called simple_publisher.py in which you will write the Python code to create a Publisher node:

gedit simple_publisher.py

Add the following code to the file:

#!/usr/bin/env python 

import rospy
from geometry_msgs.msg import Twist

if __name__=="__main__":
    rospy.init_node("my_publisher_node")
    my_publisher = rospy.Publisher("/turtle1/cmd_vel", Twist, queue_size=10)
    rate = rospy.Rate(10)
    my_velocity = Twist()
    my_velocity.linear.x = 0.5
    my_velocity.angular.z = 0.5

    while not rospy.is_shutdown():
        my_publisher.publish(my_velocity)
        rate.sleep()


Just like the Subscriber node, you start with importing the rospy module and the Twist message as we will use this message to publish to the topic “/turtle1/cmd_vel”.

#!/usr/bin/env python 

import rospy
from geometry_msgs.msg import Twist

Inside the main program, you need to initialize a ROS node, in this case with the name “my_publisher_node”. This allows ROS to identify from which node, the topic is published.

After this, you create a publisher object with the following input parameters: topic name, message type and queue size. The topic name must have the exact same name as the topic that you want to use for communicating with other nodes. The message type indicates which variable type needs to be given to the publisher. The queue size is the length of the buffer containing the messages that still need ot be published. If the node is running too slow, the messages can pile up and the overflowing messages are ignored. With a queue size of 10, this buffer will containt up to 10 old messages until it drops newer messages. If the buffer is small, the messages are always more up-to-date while a big queue size will make sure that fewer messages are lost. Small queues are usually used for messages that require a fast reaction from other nodes while big queues are used for logging or high accuracy tasks.

if __name__=="__main__":
    rospy.init_node("my_publisher_node")
    my_publisher = rospy.Publisher("/turtle1/cmd_vel", Twist, queue_size=10)

The rate defines the frequency in Hz of the node. High frequencies result in quicker reaction times but also higher computational load and more data traffic. For many cases, a rate of 10 Hz is sufficient to keep the robot running. In a few cases, where real time behavior is wanted, 100 Hz is often used, but not much higher.

Then , with the Twist() constructor, you can create a template of your message. After initializing the message object, you can modify the variable, in this case my_velocity, to meet your wanted behavior. The code below will introduce a linear velocity in x direction which is the front of the robot and an angular velocity around the z axis which is pointing upwards.

    rate = rospy.Rate(10)
    my_velocity = Twist()
    my_velocity.linear.x = 0.5
    my_velocity.angular.z = 0.5

Finally, you need a loop that will keep your ROS program running until you stop it. Otherwise, the robot would only make a small step forwards and then stop. With this loop, it will keep driving forward and turn so that it will create little circles.

The condition “rospy.is_shutdown” will be false until you press CTRL+c on your keyboard to stop the program. Until then, this while loop will keep running. Inside the loop, the my_velocity message is sent to the topic /turtle1/cmd_vel. In this scenario, the message will not be updated. In a real robot, you would also read sensor data inside this loop and change the behavior of the robot with conditional statements.

The rate.sleep() function will make sure that the loop will not go to the next iteration until a certain time has passed. This time is defined by the frequency that you specified earlier. This is necessary in each Publisher as it would otherwise run through the loop as fast as it could and overflow the topic with data. If you would use a simple function to wait for 100 ms, the speed of your loop could change depending on how log the CPU needs to process the algorithms inside the loop. As this would be unreliable and inconsistent, the sleep function is the go-to method. If the algorithm inside the loop take longer, the loop will not wait at all, and when the algorithm takes less time, it will wait until the timer to maintain the specified frequency has been reached.

    while not rospy.is_shutdown():
        my_publisher.publish(my_velocity)
        rate.sleep()

You can start your publisher node with the following command:

rosrun my_turtlesim simple_publisher.py

You will notice that the terminal does not show anything, but the turtle will start to move in a circle.

You can also verify the published messages with the following command:

rostopic echo /turtle1/cmd_vel -n 2

This command will show you the messages send to the topic /turtle1/cmd_vel, which are the Twist messages that your simple_publisher.py program is publishing. The command above will show the following:

Note that the echo command usually keeps printing the new messages but with the parameter -n 2 it only prints two messages.

That’s it. Now you are able to write nodes that can subscribe or publish to topics. In some cases, you need to combine both in a single program. For now, you can continue with Services and how to set up a Service Server and how to create a Service Client.

Creating ROS Packages

After you learned how to use a ROS package, you will learn how to create your own ROS package.

Create a Package

Creating a package is done by using the catkin environment as each ROS package is following the catkin format. First, you need to go to your catkin workspace which is called catkin_ws. Open a terminal and type:

cd ~/catkin_ws

Next, you need to enter the src directory which stores the source code of your own packages:

cd src

You can create a new ROS package with the following command:

catkin_create_pkg <package_name> <dependencie_1> <dependencie_2> <dependencie_3> <...>

Here, the <package_name> is the name of your package. Note that you can not simply change that name after you created it as you will need to modify the CMakeLists.txt file and the package.xml file. The <depenencies> are entries in the CMakeLists.txt and package.xml files that allow ROS to include some libraries that you can use. For Python programs, you need to add the dependency rospy and for C++ programs, you need to add roscpp. If you use libraries for navigation, sensor data collection or something else, you can also add these dependencies here.

For you package, you will simply add the rospy dependency and you can call the package my_turtlesim.

catkin_create_pkg my_turtlesim rospy

It is a convention to name packages with lower case names combined with an underscore. This notation is also called snake case.

Now, a new directory has been created containing a CMakeLists.txt file and the package.xml file and a folder called src. This folder is meant to contain the C++ source code. This is why many people and tutorials make a new directory for Python scripts called scripts. Therefore, type the following in your terminal:

cd ~/catkin_ws/src/my_turtlesim/
mkdir scripts

If you also have a robot model, configuration files or documentation to your package, you can create more folders and store the files in the correct folder.

Create a ROS Python Program

Now, you can create your first ROS program with Python. Enter the scripts directory with your terminal:

cd scripts

You need to create a new file with the file extension .py in order to have a Python file. You can create a new file and open it with your terminal with the following command:

gedit my_first_program.py

You will see that a new window will open on your screen with an empty text file. This is where you will write your first ROS program in Python.

Write the following code in your file and save the file:

#!/usr/bin/env python

import rospy

if __name__ == "__main__":
    rospy.init_node("my_first_node")
    rospy.loginfo("Hello World!")

Note: in order to use the ROS tools inside a Python script, you need to import rospy. Also, as every ROS program is running as a node, you need to declare a name for your node with the rospy.init_node(“my_first_node”) function. Each ROS node needs to have a name as otherwise the roscore doesn’t know which node is executing code. This means, one of the first things you want to do in your programs is to declare the name of the node.

Before you can run your program, you need to make the Python file executable, this means to give this file permissions from your system to be executed as a program. Usually, files only have the permission to be read as a file or to be modified. This is also known as read-write permission. To add the permission to execute the file, make sure you are in the scripts directory and type the following command in your terminal and press ENTER:

chmod +x my_first_program.py

Making Python files executable is required for each Python file you create but you only need to do this process once for each Python file. You can run your Python program with the following command:

rosrun my_turtlesim my_first_program.py

The terminal in which you started the Python program should output some text as you can see below:

Tip: you don’t need to type the entire command by yourself. If you start typing a word, you can double tap the TAB key on your keyboard to auto-complete the commands in your terminal.

Note: if your computer doesn’t show your ROS package, first make sure you actually have spelled the names correctly and that the package actually exists. Also, if the package is new, ROS might not know about it yet. Therefore, type the following to list all the packages on your system, after that, ROS should be able to auto-complete the name of your package as well:

rospack list

The above command will list all the installed ROS packages on your computer. As ROS is going through the entire system, it will probably find your package and add it to its known packages.

Create a launch file

You have already seen how to start a ROS program by using the rosrun command. The rosrun command allows you to start one single program at a time. In most cases, one single program will not be enough to get your robot up and running. In these cases, a launch file will make your life easier.

First, create a directory called launch to organize your package. Therefore, you must be inside your my_turtlesim folder.

cd ~/catkin_ws/src/my_turtlesim/
mkdir launch

Even though, there is no requirement to call this directory launch, it is a widely used convention. The best idea is, to stick to these conventions as they make life easier for you and your team.

Now, enter the new directory to create a new file. You can do this with your file browser or with the terminal:

cd launch

You can create a new file and open it with your terminal with the following command:

gedit turtlesim.launch

Now, a new window should open up. It shows an empty text file. The launch extension is actually a XML file type that is used to create and structure a launch file for ROS.

Type the following code into the launch file and save the file:

<?xml version="1.0" encoding="UTF-8"?>

<launch>

    <node
        name="my_node"
        pkg="my_turtlesim"
        type="my_first_program"
        output="screen"/>

</launch>

Note: unlike Python programs, you do not need to make the launch file executable.

You can simply start it with the following command in a new terminal:

roslaunch my_turtlesim turtlesim.launch

The above command will give you the following result:

You can see that the roslaunch method of starting a program outputs much more in the terminal. This is because there is much more going on. At first, ROS will check if there is already a roscore running and if not, it will start the roscore. Next, it will start the programs that are listed in the launch file.

As you added the line output=”screen” to the launch file, it will print the output on the terminal, otherwise it would not show anything from the started programs.

You can now change the content of the launch file into the following code:

<?xml version="1.0" encoding="UTF-8"?>

<launch>

    <node
        name="turtlesim_node"
        pkg="turtlesim"
        type="turtlesim_node"
        output="screen"/>

    <node
        name="draw_square"
        pkg="turtlesim"
        type="draw_square"
        output="screen"/>

</launch>

Save the file and launch it again with:

roslaunch my_turtlesim turtlesim.launch

You should now see that the turtlesim window is opening and the turtle is immediately starting to move in a square pattern. The launch file has now started two ROS programs at the same time. This makes many things much easier. On top of that, you do not need to first start the roscore as the launch file is already starting it for you.

Explanation of the Launch File

The launch file is written in XML syntax. This means you have tags that are indicating the type of content. The following tag tells your computer what type of file the launch file actually is:

<?xml version="1.0" encoding="UTF-8"?>

The content of your launch file will then be written inside the launch tags:

<launch>

</launch>

Lastly, the node tags describe what ROS program you want to run. This is indicated with the following lines:

    <node
        name="turtlesim_node"
        pkg="turtlesim"
        type="turtlesim_node"
        output="screen"/>

Here, the syntax is as follows:

    <node
        name="<name_of_the_node>"
        pkg="<name_of_the_package>"
        type="name_of_the_program_file"
        output="screen"/>

In this context, the <name_of_the_node> refers to the name you used inside the rospy.init_node() statement. As you do not know the exact name of the turtlesim_node source file, you can just assume it has the same name as the program name. The <name_of_the_package> is the name of the ROS package that contains the wanted ROS program. The <name_of_the_program_file> is the name of the Python file with the .py extension or the name of the compiled C++ program which doesn’t have any extension in Linux.

The output=”screen” will make sure that all text will still be shown on the terminal. On a robot, you do not need this but for running programs on your computer with a screen, this is helpful to see what is happening. In case you create a launch file and you don’t see output in your terminal, check if this is missing.

With this, you have learned how to run and how to create ROS packages. Next, you will learn how to subscribe and publish to Topics.

General Python Programming Advice

Here is some advice that you should keep in mind while writing your code. These tips should make it easier for you to write readable and maintainable code. The most important thing about computer code is, that you will be able to understand it even after a longer period of time. Also, you will probably read more code than write it, so make sure it makes sense to both, a different person and yourself 6 months from now.

Meaningful names

Use meaningful names for variables and functions. Have a look at the following example:

def a(h, w):
    result h*w

The above function most likely takes the height and the width of an object and calculates the rectangular area of this object and returns it. This function might even come with a comment explaining what it does. But just imagine you are at the bottom of your code and there is just this function used as such:

if a(5, 12) < 35:
    print("Too small!")

What was the use of the function a() again? Do I really need to look it up? This is even worse when you import the functions from a different file, then you cannot just scroll up to have a look.

Try to avoid such a vague name pattern and use descriptive names that make sense.

def area_rectangle(height, width):
    result height*width

if area_rectangle(5, 12) < 35:
    print("Too small!")

If you now look at the code, it does make more sense, even without any comments. Also, if you have several functions with a similar goal, you may want to use a certain naming patters such as area_rectangle() and area_circle which makes it clear by their names that they have some similarities. The same applies to the names of variables. r is not the best variable name for the radius of a circle, therefore, use radius_circle instead, or something else that is more descriptive.

You may even want to add the measurement unit inside the name such as battery_level_mV so you will immediately see that this value is given in mili-volts instead of volts.

Avoid hard-coded values

When you implement some code with numbers or strings inside that code, you could either type the values you want into the code, or you could use a variable instead. If you implement the code by typing the actual values, these values are called hard-coded as you cannot easily change them.

Why is this bad? When you have a value that changes during the development phase, you will need to find this value in the code on each instance you use this value, and replace it. This can be a very time-consuming process while hard-coded values do not have any real advantage.

In some programming languages, you can implement these values as constants which are variables whose value cannot be changed. In Python, constants do not exist so it is common practice to use a variable written entirely in capital letters.

MIN_VOLTAGE = 1.2
MAX_VOLTAGE = 1.7
PI = 3.141592
SOFTWARE = "Python"
SOFTWARE_VERSION = "2.7.17"
DEFAULT_STATE = False

In case the value will change inside the code, you should use a simple variable instead of a constant. The advantages are that you can easily change the value of these constants without searching in the code and you can see in the code what this value actually means. If there are hard-coded values, these numbers do not always make sense. Compare the following:

if voltage_mV > 1.2:
    print("Enough power left!")
if voltage_mV > MIN_VOLTAGE:
    print("Enough power left!")

The second version of that code is more likely to make sense to the reader than the version with hard-coded values.

When using constants, they are usually stored at the top of the document so they are easier to find. An alternative to constants would be to use a configuration file which would be more advanced already. Common formats for config files are JSON (JavaScript Object Notation) format or YAML (Yaml Ain’t Markup Language) format.

Comments

The use of comments in code is often discussed among developers. Why using them? Why not using them? When to use them? There is no absolute answer to this topic, but here are some guidelines that you can apply.

Avoid comments

Don’t understand this wrongly. Comments are great to describe the functionality of your code, but you should still try to limit the number of comments that you insert into your code. Why? Because it clutters the code and it can be misleading. Why can it be misleading? Well, the code is the part that actually does what is says, so if there is a change in the code, the code will still tell the truth, but when the code changes, the comments are often untouched. This means the comment tells something different as the code executes. In that way, the code can lie.

Have a look at the following example:

if light_sensor_mV > 300:
    # sets day_time to True
    is_day_time = True
else:
    # sets day_time to False
    is_day_time = False

Now it can occur, that you may change the sensor, or make a different circuit which then inverses the behavior of the sensor. You change your code so it does work properly again. But as you only verify if the code is correct, you may not realize that the comments are not matching what the code does:

if light_sensor_mV < 300:
    # sets day_time to True
    is_day_time = False
else:
    # sets day_time to False
    is_day_time = True

The issue is, that when you re-visit your own code after several months, you forget that you changed the code and then you only see that the comments and the code do not match. What does that mean? is the code wrong? Do you need to change the code? So you can now either trust the comment and change the code or you can trust your code, not knowing if it worked or not, and change the comment. This creates some ambiguity that is unnecessary and can easily be solved by removing these comments. There, it helps that your variable names and function names have a good descriptive name so that you don’t need any comments to figure out what they mean.

To make it short: Comments can lie, code cannot lie.

Write meaningful comments

But this does not mean you should never use any comments. The goal is to avoid unnecessary comments. A comment is appropriate if it adds value to the code.

There are situations where a code is simply not necessary:

# set radius to zero
radius = 0
# set center to (0, 0)
center_x = 0
center_y = 0

def print_hello():
    # prints "Hello"
    print("Hello")

The above code contains comments that are totally unnecessary. Except from the risk that the comments could be misleading, these comments here do not add value. Some developers would go so far to say that, if you have the urge to write a comment to explain the code, the code itself is not well written. I would not go so far, but I would recommend thinking if that comment is really useful or not.

Comments that can add value include explanation why you set a certain value, explain the general functionality of a complex function or add notes to remind you to add a new feature later. Some text editors even highlight the word TODO for that matter.

max_battery_voltage_mV = 3300
min_battery_voltage_mV = 2700
battery_voltage_mV = voltsensor_battery.read()

# check if voltage is enough to drive back
if battery_voltage_mV > min_battery_voltage_mV:
    # TODO: implement functions to continue
    print("Continue driving!")
else:
    # TODO: implement functions to drive back
    print("Driving back!")

You can see that the above code has a comment explaining why there is a check of the voltage and there are comments explaining what needs to be done in the near future. When these features are implemented, these TODO comments should be removed.

Before writing a comment, think whether it adds value to your code or not.

Commenting code

When testing your code or testing a new feature, you will likely comment out some code to see how the program reacts to these specific lines. This is totally fine while developing, but when there are still lines of code that are commented out after you finished your work, this will also lead to confusion. People will most likely not use this code as it is not needed, why else would it have been commented out earlier? At the same time, people will not remove this code, because why else was this code not deleted earlier? This code might be needed later? Maybe not? Other people don’t know. And you also will nit remember after a longer break.

Some people recommend to remove the code as soon as you don’t need it anymore, to avoid this confusion. By using a version control system like git, the code that has been removed is never really lost anyways. Other developers don’t like the idea of radically removing code, they might need it later. So if you want to keep the code ready to re-integrate again, add a comment explaining why you put that code into comment. Yes, this will clutter the code even more, but at least there is no confusion about why there is code into comment.

When adding some test code such as additional print statements, you may also add a comment to explain that this code is for debugging only. This makes it easier to clean up your work when it is finished and working.

Use functions

This might sound obvious, but sometimes people tend to forget to implement some specific code as a function. This might happen, because the code is just a few lines that are repeated and implementing a function seems a little bit too ambitious. Then, you want to change that code and then you need to change it several times inside your code. If this code was implemented in a function, you could easily change it one single time and then it would be fine again.

For example if you want to calculate the distance between two points:

from math import sqrt

point1 = (2, 5) # in cm
point2 = (3, 7) # in cm
distance1 = math.sqrt((poin2[0] - point1[0])**2 + (poin2[1] - point1[1])**2)

point3 = (5 -2) # in cm
point4 = (0, 3) # in cm
distance = math.sqrt((point4[0] - point3[0])**2 + (point4[1] - point3[1])**2)

In the above code, the coordinated of the points are indicated in centimeters but you want to have the distance in meters. You now need to add the conversion twice:

from math import sqrt

point1 = (2, 5) # in cm
point2 = (3, 7) # in cm
distance1 = math.sqrt((poin2[0] - point1[0])**2 + (poin2[1] - point1[1])**2)
distance2_m = distance1/100 # add conversion here

point3 = (5 -2) # in cm
point4 = (0, 3) # in cm
distance = math.sqrt((point4[0] - point3[0])**2 + (point4[1] - point3[1])**2)
distance2_m = distance2/100 # add conversion here

If you implemented a function for this, you would simply need to add the unit conversion once and the actual code would be much cleaner:

from math import sqrt

def calculate_distance(point1, point2):
    distance = math.sqrt((poin2[0] - point1[0])**2 + (poin2[1] - point1[1])**2)
    distance_meters = distance/100
    return distance

point1 = (2, 5) # in cm
point2 = (3, 7) # in cm
distance2_m = calculate_distance(point1, point2)

point3 = (5 -2) # in cm
point4 = (0, 3) # in cm
distance2_m = calculate_distance(point3, point4)

The function does not much more than the actual code, but it is easier to maintain. You can simply add or remove parts of the function and then the changes are consistent for each time you use this function. Functions are a powerful tool, so don’t hesitate to use them. Also, using functions allows you to use a meaningful name that might be more descriptive than the actual formulas used in the code.

Separate different levels of abstraction

What is a level of abstraction? When writing code, you might realize that some code is more abstract than other code. By abstract, I mean that some code is closer to the actual hardware than other pieces of code.

For example, if you have a micro-controller and you change the state of one pin from LOW to HIGH, this would be considered very low level behavior as this is very close to the real hardware and is not abstract at all.

Instead, if you use a function that simply sends a message through the serial port or just use the print() function, this is considered to be very abstract code as you do not need to worry which bits and bytes are changed in order to make your message appear on the screen or on another device. This code would be very abstract and it would probably be implemented through a set of lower level functions that do the work for you.

motor_enable_pin = True # low level code
print("Motor enabled!") # high level code

The actual advice is to keep same level of abstraction together. No high level code together with low level code in one function. How to do that? Implement a function that makes an action more abstract. In the above example, there is the changing of the voltage level of a hardware pin next to a print statement. In this case, you may want to re-organize the code as follows:

def main():
    enable_motor() # high level code


def enable_motor():
    enable_motor_pin() # mid level code
    print("Motor enabled!") # mid level code


def enable_motor_pin():
    motor_enable_pin = True # low level code

Now, in the above code, there are three functions that are responsible to enable the motor and to write a message on the screen. You may not find this very intuitive as you actually write much more code which is not doing more than the two lines earlier, but in the end, the code is more structured and easier to read.

In the end, you could say that, by writing more, you end up reading less. You only need to look at the abstraction level that you actually are interested in. If you only want to make sure the message you write on the screen is correct, you do not need to worry about which pin has been enabled or which other actions have been taking in order to enable the pin. Of course, the above example is very short. In a real world example, each function will contain more code and then the use of functions of different abstraction levels will make even more sense.

Keep it clean

Avoid the “Quick and Dirty” approach as you probably end up rewriting it several times and then spend more time as if you had done it the “Nice and Clean” way.

By “Quick and Dirty”, I mean to ignore all the advice above just because it is too much work to think of good variable and function names and not putting any value adding comments because “you know what it does”. You might end up with code that will be used more often than you could imagine and you end up wondering why this code is such a mess.

I once was told to write some code the “Quick and Dirty” way because the code should do a small task and then I would not have to bother with that code ever again. I ended up working over 8 month with that same code and I wished I just made it “Nice and Clean” in the first place instead of trying to improve it each time that I re-open the code.

If you are not sue about certain topics, you can go back to Object-Oriented Programming.

Object-Oriented Programming

You can see Object-Oriented Programming (OOP) somewhat like a more advanced topic in Python where Python does implement it pretty well. Even though Python does not require an OOP approach for making complex programs, it sometimes makes life easier while sometimes it doesn’t. As you will most likely get in touch with some code that has been written in an Object-Oriented approach, it is at least good to have seen the concepts of it.

While Python scripts work pretty well without any implementation of Object-Oriented Programming, it allows you to keep your code even more structured in form of Classes, Attributes and Methods.

Classes

In simple terms, a Class is a group of related variables and functions, all bound to a so-called Object. Except, in the scope of OOP, you call the variables Attributes and the functions are called Methods. These objects are meant to behave similarly to real-world objects with individual properties and things that can be done with them. The syntax of creating a class is fairly easy:

class Robot:
    # remaining code here

As you can see, the class is introduced with the keyword class. Next, there is the name of the class, in this case, it is called Robot. By convention, class names are written with the first letter as a capital letter, the remaining lowercase. If the class name is a compound word such as “Car Battery”, you would write each word starting with a capital letter: CarBattery.

Then, there is another “keyword” called self. In this case, self is not really a keyword of Python as you could call it whatever you like, but the word self has become a convention. In other languages like Java, the fixed keyword would be this where you cannot change the keyword there. In python, you could call it john, roboshack or whatever you like. Sticking to the convention of self isn’t a bad idea though.

The final thing to mention about the class declaration is that it ends with a colon and the following code, that is belonging to the class, is indented, similar to an if-statement or a loop.

Attributes

As already mentioned, the Attributes are variables that belong to the class object, just like the properties of a real-world object. A robot for example can have a name, a number of wheels, a number of robotic arms, and battery voltage. These Attributes would then be introduced as such:

class Robot:
    name = "Sony"
    nr_wheels = 4
    nr_robotic_arms = 0
    battery_level_volt = 12.3

These Attributes are easily accessible within the function, but also outside of the function. Notice that these attributes are still part of that Object and do not behave in the same way as a global variable. (The scope of variables will be covered later in another part.)

Before you learn how to actually use this Class, let’s have a look at the last part: the Methods.

Methods

A Method is basically a function that is tied to an Object of a Class. The Method can easily make use of its Class Attributes, again through the self keyword. One special thing is, that a Method always takes at least one argument: self.

class Robot:
    say_hi(self):
        print("Hi!")

The above Method doesn’t make use of its Attributes, but the following for example does:

class Robot:
    battery_level_volt = 12.3
    battery_is_full = True

    check_battery(self):
        if self.battery_level_volt > 12.2:
            print("Battery is still charged!")
            self.battery_is_full = True
        else:
            print("Battery is getting empty!")
            self.battery_is_full = False

As you can see, the above example is not receiving the actual battery voltage as an argument and yet it can access the battery status through the self keyword. This can help to unclutter functions that would otherwise take quite a lot of input arguments to work with. If you still want to pass some arguments, you can simply add them after the argument self, separated with a comma:

class Robot:
    battery_level_volt = 12.3
    battery_is_full = True

    battery_charged(self, battery_voltage):
        if battery_voltage > 12.2:
            print("Battery is still charged!")
            return True
        else:
            print("Battery is getting empty!")
            return False

The latter solution is less elegant as it requires additional input and it will give some additional output that might be stored in the battery_is_full Attribute anyways. Additional input parameters make more sense when they contain information from outside of the Class such as a distance to travel or sensor data.

Instances

When you create a new variable of the type of a Class, you actually create an Instance of that Class. You can even make several Instances of the same class. This happens as such:

class Robot:
    name = "Sony"
    battery_level_volt = 12.3
    battery_is_full = True

    check_battery(self):
        if self.battery_level_volt > 12.2:
            print("Battery is still charged!")
            self.battery_is_full = True
        else:
            print("Battery is getting empty!")
            self.battery_is_full = False

my_robot = Robot()
my_robot.name = "Turbo"
my_robot.battery_level_volt = 13.1
my_robot.check_battery()

The above code will create an instance of a Robot and then save some data into its Attributes and even call a Method of that class. Now, assuming that the class is already given as before, the following example would create two more instances of that Class:

robot1 = Robot()
robot1.name = "Nitro"
robot1.battery_level_volt = 12.7
robot1.check_battery()

robot2 = Robot()
robot2.name = "Speedy"
robot2.battery_level_volt = 12.0
robot2.check_battery()

As these two robots above are different instances of the same Class, they both have the same Attributes, but they have different values for their Attributes. This keeps things well organized and reduces the risk to override the properties of one object when handling another object. The Method robot1.check_battery() will only check the battery voltage of robot1 and it will not bother about robot2. This is done by using the self.battery_level_volt Attribute inside the Method definition. It will only check the voltage of its own robot.

Constructors

There is one special type of Method, the so-called Constructor of the class. It is actually called when you type:

my_robot = Robot()

The above line calls the Constructor of the Class. This means that there, Python creates all the Attributes and Methods related to that Object. In addition, this Constructor is able to call a special internal Method with the name init surrounded by two underscores:

class Robot:
    name = "Sony"
    battery_level_volt = 12.3
    battery_is_full = True

    __init__(self, name, battery_voltage):
        self.name = name
        self.battery_level_volt = battery_voltage
        self.check_battery()

    check_battery(self):
        if self.battery_level_volt > 12.2:
            print("Battery is still charged!")
            self.battery_is_full = True
        else:
            print("Battery is getting empty!")
            self.battery_is_full = False

my_robot = Robot("Ronny", 12.4)

Now, when the Constructor Robot(“Ronny”, 12.4) is called, it will automatically call the __init__() Method and it will set the name and the battery voltage to the parameters given to the Constructor. In addition, it will call the check_battery() Method. This can save some work to initialize the object or take some actions that you would otherwise do anyways right after creating a new Instance of that Class.

Inheritance and Polymorphism

Just like in other programming languages, Python classes can be based on other classes. Also, you can overwrite existing methods and overload functions. These topics will not be explained in this guide as they are already more advanced and they would make things even more complicated as they already are at this point. For simple Python programs, and even simple scripts for robotics programming, these concepts are not really necessary at this point in time.

Okay, but why to use Classes?

If you think, this looks more complicated than simply making a bunch of non-OOP variables such as in the following example, you will be proven wrong:

robot1_name = "Nitro"
robot1_battery_level_volt = 12.7
battery1_is_charged = robot_check_battery(robot1_battery_level_volt)

robot2_name = "Speedy"
robot2_battery_level_volt = 12.0
battery2_is_charged = robot_check_battery(robot2_battery_level_volt)

Keeping things organized

At the first sight, the above might look easier to implement. But one big disadvantage simply is, that these are all loose variables. This means if you want to make two instances of the same object, without using a class, each property has its own variable. If you want to copy all the parameters into another variable, you need to specify every single variable. The same holds true if you want to pass all these properties as a parameter into a function, you will need to enter each variable name and while doing so, for each variable you are potentially prone to make a typo.

Without using a class, you will be likely to do the following if you want to copy all the values from one instance to another one:

robot1_name = "Nitro"
robot1_battery_level_volt = 12.7
battery1_is_charged = robot_check_battery(robot1_battery_level_volt)

robot2_name = robot1_name
robot2_battery_level_volt = robot1_battery_level_volt
battery2_is_charged = battery1_is_charged

To be fair, you could do it in one single line in Python:

robot2_name, robot2_battery_level_volt, battery2_is_charged = robot1_name, robot1_battery_level_volt, battery1_is_charged

The one-line solution does work, but it doesn’t make it more readable in the case you have many variables. This will be more elegant if you’d just implemented a class containing all the properties and attributes you need:

robot1 = Robot("Nitro", 12.4)
robot2 = robot1

Now, your code has copied every class attribute from robot1 into robot2 without a high likelihood of making a typo, no risk to forget one variable and in a very readable way.

Keep things inside a class

Another advantage of implementing a class is that each method has easy access to its class attributes through the self specifier. Also, you can still access the attributes from outside the classes (in Python, you cannot make a variable private or protected like in Java or other languages). As a result, you can make the code easier to read and write as you don’t need to specify every attribute as a parameter for a method call. You just access the data from within the method. In the same way, you can modify the class attributes without the need of having a return value.

So instead of the following code:

robot1_name = "Nitro"
robot1_battery_level_volt = 12.7
battery1_is_charged = robot_check_battery(robot1_battery_level_volt)

You simply have the following code in the OOP approach:

robot1 = Robot("Nitro", 12.7)
robot1.robot_check_battery()

The Object-Oriented code is shorter, easy to read and modify and it does not contain any unnecessary information. The complexity is more or less hidden inside the method definitions which is fine as you only write the method once, but you can use it as often as you want.

Modularity

Another important aspect of Object-Oriented Programming is the modularity of the code. You can easily write an external file containing the class definition and then import it to the Python file and make use of the class. This makes the source code more structured and at the same time, you can easily copy the file containing the class definition into other projects. For example, if you use import rospy, you do exactly that, you import the file containing class definitions for ROS.

Use external code

Even though you may get along pretty well without using classes, you might receive code from a colleague or you find an example on the internet and it has been implemented using OOP. In this case, it is still useful to be familiar with the OOP approach of programming in Python as it is a widely used approach. After all, OOP is there to make things easier. Even though you might want to avoid that initial hurdle to understand how it works, it will probably get to you in any unexpected way.

This is it! Now you know a wide basis of the Python programming language. There are more aspects that have not been covered in detail or not at all. But the parts that have been discussed so far should make it easier for you to get started.

Continue learning about general python programming advice or go back to revisit functions.

Functions

After you saw how to use loops, what do you use when you want to repeat the same code several times? Sure, a loop! But what if you want to run the same code, but only once every now and then? Just copy the code every time you want to run that code? Why not use a function?

What are functions?

A function can be seen as a mini program inside your program. They are very useful to avoid coping and pasting the same blocks of code over and over again. The difference to a loop is that a function is usually used to group some functionality together and repeat the same algorithms at different places inside your program instead of repeating it several times after each other like a loop does.

You probably know the concept of a mathematical function such as: f(x) = x². This kind of functions can also be used in programming:

def f(x):
    return x**2

The above function is representing the function f(x) = x². The key word def is used to indicate that there will be a new function definition. After the key word def, the name of the function is defined, in this case simply ‘f’. Inside the parentheses is the parameter that can be given to the function. This is a way to give your mini program some input. A function can have several input parameters, they are then separated by a comma. Then the definition ends with a colon and the next lines that are indented. Everything inside the same level of indentation is part of that function definition. The last element of the function is the return key word. The variable(s) after the return key word are given as an output of your function.

You can call the function as follows:

y = f(3)

Now, the return value will be stored inside the variable y. Like this, a simple formula such as a²+b²=c² could be easily implemented as:

a_squared = f(a)
b_squared = f(b)
c_squared = a_squared + b_squared

In case you as: Why using a function for such a simple thing? Well, you probably will not use it for such a thing. Most likely, you will use it for more complex calculations. Also, you will hopefully not call the function ‘f’ and the input parameter not ‘x’. Let’s have a look at another function. The following will calculate the surface area of a rectangle with two input parameters:

def rectangle_surface_area(height, length):
    return height * length

The above function has a descriptive name, and the parameters are explicitly telling what they are supposed to be. This is often seen as good practice for writing functions.

A function can be more than a simple line of calculations. It can even contain print statements, loops and even other functions.

Small Example

The following function will ask the user to input his name and his age.

def ask_personal_data():
    name = input("Please enter your name: ")
    age = input("Please enter your age: ")
    return name, age

The function will then be used as such:

user_name, user_age = ask_personal_data()

if user_name == "John":
    print("Hello John! Did you know that this is a very common name?")
else:
    print("Hello!")

As said, the above function will ask the user to type the name and the age and then the function returns both values. The user will see something like this:

Please enter your name:
Jacob
Please enter your age:
25

Advantages of functions

With this function, you can now use it everywhere in your code to perform the same tasks again and again where they are needed. One advantage is that they make the program sorter and easier to read. Another advantage is, that you only need to modify the code one single time instead of modifying it on each occasion where you use the same algorithm.

Increased Maintainability

Let’s have a look at the following example of a driving robot:

motor_speed = 0.2 # m/s

sensor_input = 540 #assume: 0 -> 0 meter and 1024 -> 2 meter
distance_to_wall = sensor_input * 2 / 1024
if distance_to_wall <= 0.2:
    print("You need to stop now!")
    motor_speed = 0

# read user input

sensor_input = 370 #assume: 0 -> 0 meter and 1024 -> 2 meter
distance_to_wall = sensor_input * 2 / 1024
if distance_to_wall <= 0.2:
    print("You need to stop now!")
    motor_speed = 0

# calculate battery voltage

sensor_input = 210 #assume: 0 -> 0 meter and 1024 -> 2 meter
distance_to_wall = sensor_input * 2 / 1024
if distance_to_wall <= 0.2:
    print("You need to stop now!")
    motor_speed = 0

Now let’s see how this could be reduced with a function:

def calculate_distance(sensor_input, motor_speed):
    sensor_input = 540 #assume: 0 -> 0 meter and 1024 -> 2 meter
    distance_to_wall = sensor_input * 2 / 1024
    if distance_to_wall <= 0.2:
        print("You need to stop now!")
        motor_speed = 0
    return motor_speed

motor_speed = 0.2 # m/s

motor_speed = calculate_distance(540, motor_speed)

# calculate battery voltage

motor_speed = calculate_distance(370, motor_speed)

# read user input

motor_speed = calculate_distance(210, motor_speed)

Now you might ask: where is here an advantage in number of code lines? The first option only has 4 lines more than the second option. Yes, this is correct, the difference is not that big indeed.

However, imagine you decide not to use the distance in meters as you will always get small decimal values but instead, you want to use centimeters instead. In the first option, you need to modify the calculation of the distance three times. In the second option, you only need to do it once. Also, if you use the same calculations 20 times inside your program, you don’t always think of each time you used it and you might forget it at one spot and then there are mistakes in the code. Or imagine you have a different sensor with a range from 0 to 2048 bits instead of 1024. This means many changes all over the code or only one single change in the function.

Increased Readability

Not convinced yet? The following function will not save you many lines, but the calculation inside is not very self-explanatory. The function name will tell you more about what you are calculating.

# import the square-root function from the math module
from math import sqrt

def distance_between_points(x1, x2, y1, y2):
    distance = sqrt((x2-x1)**2 + (y2-y1)**2)
    return distance

Functions can also be used simply to structure your code into logical chunks or to give meaningful names to the algorithms that you program, even if you know you will use this algorithm only once in this program.

Modularity

Also, you might notices the line from math import sqrt. This line imports a function from another python file, so you can use it in your code as well. This means functions make the software more modular and reusable. There are many more functions that you already saw like the input() function, the print() function (yes, technically in Python 2, the print function is not a real function but a statement but in Python 3 it is a function) and here the sqrt() function.

So you have been using functions all along until now, without really noticing it. And this is why people use functions, to make their lives easier and keep the complexity hidden in other places. You can define the functions inside the same file as the main program or in a separate file and then import the functions from this file.

Importing functions from other Python files (so called Python modules) allow developers to make an Artificial Intelligence (AI) application with less than 50 lines of code. The total code behind this can then be several thousands of lines, but the part that the developer is writing is only a few lines long.

Additional Notes

Using functions can make life easier, even though they appear more difficult at first. One thing you need to consider is, that they require an input and an output in case they should modify a value such as in the example above:

motor_speed = calculate_distance(210, motor_speed)

The example without the function could always use the motor_speed variable while when sing functions, you need to hand this variable over to the function as a parameter as otherwise, there will be an error that the function is not defined. This is due to the fact that variables can have different scopes. Also, if you want to change the value of a variable, you need to return it as otherwise the actual variable has not been changed. When calling a function with input parameters, the functions makes a copy of the variable values and does not change the original variables. Again, this has also to do with the scope of the variables. Later, you will learn more bout variable scopes.

In case you want to dive even deeper into the topic of functions, have a look at the article Concise Notes on Functions in Python.

Continue learning about Object-Oriented Programming (OOP) in Python or g back to revisit loops.