# Tutorial 1

# Your first publisher and subscriber

# Prerequisites

All that follows is assuming you have successfully completed

🛠️ Setup
../setup/
and you have a working docker container. If you haven't done so, take the time to do so now. If you have hit a roadblock in the setup, ask a lead for assistance (or google it and try to figure it out on your own...)


# Step 0: Create the package

In order to write our nodes, we need to first create its package. This is will simply give us the scaffolding for a project, so that we have all the resources we need.

If you haven't already, go ahead and launch your docker and make sure you're attached to it (you should see that big yellow [DOCKER] on your command prompt). Go ahead and navigate to your src directory (cd src). Then, to create the package, simply run

ros2 pkg create --build-type ament_python py_pubsub

Where ros2 is the executable we're running, pkg tells ros2 we want to do something with a package, create tells it we want to create one, --build-type ament_python tells the package creation tool that we want a pure python package, and py_pubsub is the name of the package.


Now, navigate to the source directory of your new package at py_pubsub/py_pubsub/, and create/open a new file called publisher_member_function.py.

We'll now write the node step by step. You can either read these sections step by step and then copy based off the full file shown later, or you can copy with each step by step.


# Step 0.5: What are we building?

We are going to build a simple publisher node and subscriber node. The publisher node will publish some message to 'topic' on some repeated timed interval, and the subscriber node will read these messages and print them out for us.


# Step 1: Writing the publisher

Let's get coding!

We'll start by importing the necessary libraries: rclpy (ROS Client Library for Python) and the object Node from rclpy.node:

import rclpy
from rclpy.node import Node

Next, we need to make sure we have the object we want to be able to send with our publisher. So, we'll go ahead and import the standard "String" message since all we want to send is text:

from std_msgs.msg import String

Now we begin the body of the code, which we'll start by creating our publisher node's python class. Python's inheritance is out of the scope of this tutorial, and isn't needed to understand what's going on, but for those who want to take the time to learn it can look at a good inheritance explainer by GeeksForGeeks:

Inheritance in Python
https://www.geeksforgeeks.org/inheritance-in-python/#
You'll see Node in the parentheses of the class definition because we want our publisher node to be built on top of the Node class:

class MinimalPublisher(Node):

All python classes require a constructor to be defined via __init__(self), and this node is no different.

We'll start by calling the constructor of the class we're inheriting from and giving our node a name via super().__init__('minimal_publisher').

Next, we'll initialize a publisher object and make it a class member by adding self.publisher = self.create_publisher(String, 'topic', 10). This makes self.publisher our node's publisher object (which we'll call to publish) by calling the inherited class Node's function create_publisher(msg_type, topic_name, queue_size) (the queue_size parameter denotes the allowed amount of queued messages if a subscriber isn't receiving them fast enough).

Finally, we'll define our timer interval, create the timer object, and initialize a member index counter to 0.

All together, we get

def __init__(self):
    super().__init__('minimal_publisher')
    self.publisher_ = self.create_publisher(String, 'topic', 10)
    
    timer_period = 0.5
    
    # self.timer_callback refers to a function we'll define later that we want the timer
    # object to call every time the interval completes
    self.timer = self.create_timer(timer_period, self.timer_callback)
    self.i = 0

Now we want to define timer_callback() so that the timer has something to do. Here we'll create a new message by initializing a String() object, we'll define the data part of the message, then we'll call self.publisher_ to publish our message, then finishing the function by logging that we've published a message and incrementing our counter variable:

def timer_callback(self):
        msg = String()
        msg.data = f"Hello world: {self.i}"
        self.publisher_.publish(msg)
        self.get_logger().info(f"Publishing: \"{msg.data}\"")
        self.i += 1

Finally, we'll create the main function for the script, so that python knows what to do when ROS runs our package. Unlike the previous two functions, this is not supposed to be a part of the MinimalPublisher node.

Here we'll initialize rclpy, initialize our publisher node, "spin" up the node (start it), and then tell it to destroy and shutdown once it's done:

def main(args=None):
    rclpy.init(args=args)
    
    minimal_publisher = MinimalPublisher()
    
    rclpy.spin(minimal_publisher)
    
    # We won't reach this point until the publisher has been told to stop running
    minimal_publisher.destroy_node()
    
    rclpy.shutdown()

if __name__ == "__main__":
    main()

Just like that, our publisher is done! All together, this script should look like this:


import rclpy
from rclpy.node import Node

from std_msgs.msg import String


class MinimalPublisher(Node):
    def __init__(self):
        super().__init__('minimal_publisher')
        self.publisher_ = self.create_publisher(String, 'topic', 10)
        timer_period = 0.5
        self.timer = self.create_timer(timer_period, self.timer_callback)
        self.i = 0
        
    def timer_callback(self):
        msg = String()
        msg.data = f"Hello world: {self.i}"
        self.publisher_.publish(msg)
        self.get_logger().info(f"Publishing: \"{msg.data}\"")
        self.i += 1
        
        
def main(args=None):
    rclpy.init(args=args)
    
    minimal_publisher = MinimalPublisher()
    
    rclpy.spin(minimal_publisher)
    
    minimal_publisher.destroy_node()
    
    rclpy.shutdown()


if __name__ == "__main__":
    main()

# Step 2: Writing the subscriber

In the same place as the previous file, now create the file subscriber_member_function.py. Go ahead and open it and let's start writing!

This node is almost the exact same as the publisher, with a few select differences. Rather than step by step through the entire script again, we're just going to speak on the differences.

The imports are the exact same, but the class has a different name now -- MinimalSubscriber. Thus, the first section is simply

import rclpy
from rclpy.node import Node

from std_msgs.msg import String

class MinimalSubscriber(Node):

Within the constructor, we'll want to change the name to 'minimal_subscriber', and we'll want to define a subscriber object rather than a publisher object, giving us:

    def __init__(self):
        super().__init__('minimal_subscriber')
        self.subscription = self.create_subscription(
            String, # Message type
            'topic', # topic name
            self.listener_callback, # Function to call whenever we receive a message
            10 # Queue size
        )

        self.subscription

Now completely different from the publisher is the callback function. We don't care about any timers or publishers, we just want to print out our message whenever we get it. The callback you put into the self.create_subscription function will have one parameter (aside from self as it is a member function), that parameter being the received message. Thus, our listener callback should look like the following:

    def listener_callback(self, msg):
        self.get_logger().info(f"Recieved msg: \"{msg.data}\"")

Finally, the main function and body are the exact same, just changing names to reflect the fact that this is the subscriber and not the publisher.

The full script should look like this:

import rclpy
from rclpy.node import Node

from std_msgs.msg import String

class MinimalSubscriber(Node):
    def __init__(self):
        super().__init__('minimal_subscriber')
        self.subscription = self.create_subscription(
            String,
            'topic',
            self.listener_callback,
            10
        )
        
        self.subscription
        
    def listener_callback(self, msg):
        self.get_logger().info(f"Recieved msg: \"{msg.data}\"")
        
        
def main(args=None):
    rclpy.init(args=args)
    
    minimal_subscriber = MinimalSubscriber()
    
    rclpy.spin(minimal_subscriber)
    
    minimal_subscriber.destroy_node()
    rclpy.shutdown()
    
    
if __name__ == "__main__":
    main()

Now that we have both the publisher and subscriber, we need to configure the package to make sure we have all the required dependencies to run it.


# Step 3: Configuring the package

Contained one folder back from where we stored our source code just now are a couple important files, setup.py, setup.cfg, and package.xml.

setup.py tells Python how your project is structured and what files to run if the project is asked to execute some code, setup.cfg are various ROS building arguments, and package.xml contains important information about the package as a whole, like the project's dependencies. We have some dependencies to add, so go ahead and open package.xml for editing.

Underneath some metainfo definitions like <license>, you should see a handful of lines which read along the lines of <test_depend> (something) </test_depend>. Above those statements and below the metainfo, we want to add our two dependencies:

  <exec_depend>rclpy</exec_depend>
  <exec_depend>std_msgs</exec_depend>

Next, open up setup.py so that we can add "entrypoints" -- essentially telling python what code to run if it's asked to run "talker" or "listener". Add the following to "entry_points":

entry_points={
        'console_scripts': [
                'talker = py_pubsub.publisher_member_function:main',
                'listener = py_pubsub.subscriber_member_function:main',
        ]},

# Step 4: Building & Running

To start, navigate to the root of the workspace (/ros2_ws/).

The docker image should automatically do the following step, but it's good to do yourself anyway for redundancy. We'll just make sure we have all of our ROS dependencies by running

rosdep install -i --from-path src --rosdistro iron -y

Next (still in the workspace root), we'll build the project via

colcon build --packages-select py_pubsub

and then source install/setup.bash

Now with tmux up and running, in one pane go ahead and run

ros2 run py_pubsub talker

to start the publisher. On that pane, you should start seeing some output like

[INFO] [minimal_publisher]: Publishing: "Hello World: 0"
[INFO] [minimal_publisher]: Publishing: "Hello World: 1"
...

Next, move over to your other pane and run ros2 run py_pubsub listener to start the subscriber. Once this node is up and running, it should begin outputting the messages it's receiving from the publisher at that moment. For example:

[INFO] [minimal_subscriber]: Received msg:: "Hello World: 20"
[INFO] [minimal_subscriber]: Received msg:: "Hello World: 21"
[INFO] [minimal_subscriber]: Received msg:: "Hello World: 22"
[INFO] [minimal_subscriber]: Received msg:: "Hello World: 23"
[INFO] [minimal_subscriber]: Received msg:: "Hello World: 24"

Congrats, your first publisher and subscriber nodes are up and running! To turn them off just Ctrl+c in their respective panes. You can exit tmux by either running exit within each pane or using Ctrl+b, x to close each pane (it will prompt asking if you want to close the pane, to which you will confirm by just pressing 'y').