OOP with ROS in Python

You are starting to develop with ROS and come from a programming background. Maybe you’re already using OOP in many of your programs, and you wonder how you can use OOP with ROS in Python.

In this post I’ll show you a complete Python example, without OOP, and then with OOP. The code will contain a ROS publisher, a ROS subscriber and a ROS service, so you’ll have a good overview of the ROS basics using object oriented programming.

The application is quite basic: it’s simply a number counter, with those functionalities:

  • The ROS subscriber is used to get a number from an external output. Upon reception, the number will be added to a global counter.
  • The ROS publisher will publish the new counter as soon as a number has been received and added to the existing counter.
  • The ROS service is used to reset the counter. If you call this service, the counter value will come back to 0.

Alright, now let’s write the ROS code in Python!

The Python ROS program without OOP

Complete Python Code

#!/usr/bin/env python

import rospy
from std_msgs.msg import Int64
from std_srvs.srv import SetBool

counter = 0
pub = None

def callback_number(msg):
    global counter
    counter += msg.data
    new_msg = Int64()
    new_msg.data = counter
    pub.publish(new_msg)

def callback_reset_counter(req):
    if req.data:
        global counter
        counter = 0
        return True, "Counter has been successfully reset"
    return False, "Counter has not been reset"

if __name__ == '__main__':
    rospy.init_node('number_counter') 

    sub = rospy.Subscriber("/number", Int64, callback_number)
    pub = rospy.Publisher("/number_count", Int64, queue_size=10)
    reset_service = rospy.Service("/reset_counter", SetBool, callback_reset_counter)

    rospy.spin()

Let’s break down the code line by line

#!/usr/bin/env python

import rospy
from std_msgs.msg import Int64
from std_srvs.srv import SetBool

Here we import rospy, so we can use the basic Python ROS functionalities. We also import Int64 from the std_msgs package, and SetBool from the std_srvs package. The Int64 message contains – as you can guess – a 64 bit integer. The SetBool service request part is a simple boolean, and the response part is a boolean working as a success flag, followed by a string to give a more detailed message.

counter = 0
pub = None

We initialize a global counter as well as a publisher. If we want to use them in all the functions of the program, we have to declare them in the global scope, which is far from optimal. We’ll see later that OOP code will solve that problem.

def callback_number(msg):
    global counter
    counter += msg.data
    new_msg = Int64()
    new_msg.data = counter
    pub.publish(new_msg)

This is the callback for the ROS subscriber. The received data is a 64-bit integer. What we do here is add the data to the counter declared on the global scope. We use the word “global” before the variable “counter” so we’re able to modify its value.

After updating the global counter, we publish it on the ROS publisher (also with a 64-bit integer).

def callback_reset_counter(req):
    if req.data:
        global counter
        counter = 0
        return True, "Counter has been successfully reset"
    return False, "Counter has not been reset"

This is the callback for the ROS service which is in charge of resetting the counter. Here we check if the boolean inside the SetBool service request is true. If yes, then we simply set the global counter to zero, and return a success flag and message.

if __name__ == '__main__':
    rospy.init_node('number_counter')

    sub = rospy.Subscriber("/number", Int64, callback_number)
    pub = rospy.Publisher("/number_count", Int64, queue_size=10)
    reset_service = rospy.Service("/reset_counter", SetBool, callback_reset_counter)

    rospy.spin()

The main function of the program. First we initialize the node with the init_node() function. We can then create the subscriber, the publisher, and the service server. Note that the publisher variable was declared previously in the global scope so we can use it on the callback_number() function.

After having started all the ROS functionalities, don’t forget to add rospy.spin() to your program. This will keep the program alive (and trigger your callbacks) until you kill the node. If you forget this line, your program will simply exit!

What to think about this code

Well, this program is quite simple. It does a very simple thing, and has no special dependencies on other programs. But, if you’re coming from a programming background, and are used to OOP, you should feel that something’s not right.

First, declaring global variables. I know some people are allergic to that. And it’s understandable. Imagine if your program grows, with many publishers, subscribers, services, action servers, … And we haven’t even started with threads. Sharing variables between threads in a program, while scaling the application, will probably result in you making some nightmares.

Let’s see what happens when we use OOP with ROS in Python.

The Python ROS program with OOP

Complete Python code

#!/usr/bin/env python

import rospy
from std_msgs.msg import Int64
from std_srvs.srv import SetBool

class NumberCounter:

    def __init__(self):
        self.counter = 0
        self.number_subscriber = rospy.Subscriber("/number", Int64, self.callback_number)
        self.pub = rospy.Publisher("/number_count", Int64, queue_size=10)
        self.reset_service = rospy.Service("/reset_counter", SetBool, self.callback_reset_counter)

    def callback_number(self, msg):
        self.counter += msg.data
        new_msg = Int64()
        new_msg.data = self.counter
        self.pub.publish(new_msg)

    def callback_reset_counter(self, req):
        if req.data:
            self.counter = 0
            return True, "Counter has been successfully reset"
        return False, "Counter has not been reset"

if __name__ == '__main__':
    rospy.init_node('number_counter')
    NumberCounter()
    rospy.spin()

Let’s break down the code line by line

#!/usr/bin/env python

import rospy
from std_msgs.msg import Int64
from std_srvs.srv import SetBool

Nothing changes here, we still need rospy, and we still need the Int64 message as well as the SetBool service.

class NumberCounter:

    def __init__(self):
        self.counter = 0
        self.number_subscriber = rospy.Subscriber("/number", Int64, self.callback_number)
        self.pub = rospy.Publisher("/number_count", Int64, queue_size=10)
        self.reset_service = rospy.Service("/reset_counter", SetBool, self.callback_reset_counter)

Here we create the NumberCounter class. All the code related to the counter functionality will be written inside this class.

The __init__() function is the constructor of any Python class. We initialize the subscriber, publisher, and service server directly here. Also, see the “counter” variable. It’s now part of the class. This way, we’ll be able to use any of those attributes in all the methods of the class, without having to hack something with global variables.

Note: don’t forget to add “self.” before your variables, otherwise they will exist only in the scope of the __init__() function, and not in the class scope.

    def callback_number(self, msg):
        self.counter += msg.data
        new_msg = Int64()
        new_msg.data = self.counter
        self.pub.publish(new_msg)

This method is the same as the function we wrote without OOP. But here, we use the counter attribute of the class instead of a global variable. The same applies for the publisher. We don’t need to worry where we declared it before. As soon as it’s declared in the constructor of the class, you can access it from any method of the class.

    def callback_reset_counter(self, req):
        if req.data:
            self.counter = 0
            return True, "Counter has been successfully reset"
        return False, "Counter has not been reset"

Again, this method is quite similar to what we’ve done before.

Note: when writing Python classes, don’t forget to add a “self” parameter to all your method in the class. This parameter should be the first parameter of the method.

if __name__ == '__main__':
    rospy.init_node('number_counter')
    NumberCounter()
    rospy.spin()

And finally the main function of the program. As you can see it’s quite small now, and very simple. We still initialize the node and call rospy.spin() to keep the program alive. But now, everything which is specific to the application is reduced to only one line.

We just need to create an instance of the NumberCounter class. This will call the constructor of the class, which will initialize all the variables and ROS functionalities that we need. The NumberCounter class will take care of itself and run independently.

The advantages of using OOP with ROS in Python

You can clearly see the advantages of using OOP for this code example. No more global variables that you have to declare before a specific function. No more “hacking” to make things communicate between each other. Also, by using classes it will be easier to create reusable blocks that will allow you to scale your application in an easier way.

Let’s say that you’re creating a ROS driver for a motor. You could have one class storing the data of the motor, one class to execute a control loop to communicate with the motor, one class for the communication protocol, one class to make a bridge between ROS topics and the data you send to your motor, etc etc. And then, making all those classes working with each other would be quite easy, provided that you correctly wrote the classes.

Of course, OOP is not the solution to everything, and it’s not the only solution. I’m not claiming that OOP with ROS in Python is the best solution we could use to make this code example better. If you prefer other paradigms when programming with ROS, then go ahead, and share them in the comment section below! I’d be curious to hear about that.

In the next tutorial, you’ll see how to write the same OOP code example with C++.

Leave a Comment