November 2, 2024

How to create publisher and subscriber nodes in ROS 2 from Scratch – ROS 2 detailed tutorial

In this Robot Operating System 2, or ROS 2 tutorial, we explain how to create ROS2 publisher and subscriber nodes from scratch in Ubuntu Linux and Python. In particular, we explain

  1. How to create and build ROS2 workspaces.
  2. How to create and build ROS2 packages.
  3. How to write Python source files of subscriber and publisher nodes.
  4. How to incorporate the written subscriber and publisher nodes in the package.
  5. How to properly source the new workspace and how to run the created subscriber and publisher nodes.

This tutorial is based on the Iron Irwini ROS 2 distribution. The YouTube tutorial accompanying this webpage tutorial is given below.

STEP 1: Create an Empty Workspace

First, we need to source the environment

source /opt/ros/iron/setup.bash

Here, before we proceed, we need to explain the ROS 2 concepts of underlay and overlay.

Every time we want to create a ROS 2 workspace with packages or run a ROS 2 node, we need to set up the ROS 2 environment by sourcing the setup script provided by the binary installation of ROS 2. That is, we need to execute the file /opt/ros/iron/setup.bash. This will set up the environment and create an underlay environment or underlay workspace, or simply underlay on top of which we will build our workspace.  On the other hand, by creating the workspace with packages in ROS 2, we create an overlay on top of the current ROS 2 installation, that is, on top of the underlay. After creating the workspace with packages we also have to source the setup file for that workspace. In that way, we create the workspace overlay on top of the ROS 2 installation underlay.

Similarly to ROS1, we need to create a workspace folder and a src subfolder. We do that by typing

mkdir -p ~/workspace/src

Usually, the packages are placed in the src folder. Here, to illustrate the structure of the workspace folder after the build command is called, we run the build command in an empty workspace folder. Packages will not be built since there are no source files of the package. However, the build command will create several folders in the ~/workspace folder. Later on, after we create a package, we will run the build command again.

To build the complete workspace and any possible package (if it exists), we type

cd ~/workspace/

colcon build

“colcon build” is a version of ROS1 catkin_make command used to build the workspace. The command “colcon build” will create three additional folders in the folder ~/workspace/ . These folders are:

  • build – this folder is used to store intermediate files. For every package, we will have a folder inside of the build folder. In these subfolders, CMake will be executed.
  • install – the packages will be installed in the install folder. There will be a separate folder in the install folder for every package.
  • log–  is the log folder where logging information about the “colcon build” will be stored.

STEP 2: Crate Package and Python Source Files of Publisher and Subscriber Nodes

It is important to keep in mind that the packages are created in the src folder and not in the root workspace folder “~/workspace/”. Let us navigate to the source folder by typing

cd ~/workspace/src

To create a package, we then type

ros2 pkg create --build-type ament_python first_package

The name of the package is “first_package”. Now, inside the source folder, we will have another folder with the name of our package: first_package. Let us navigate to that folder and let us inspect the structure of that folder

cd ~/workspace/src/first_package

ls -l

Inside this folder, there are several folders and files. For us, the most important folder is the folder with the same name as the name of the package. That is the folder called “first_package”. In that folder, we will place the source files of our publisher and subscriber nodes. Let us navigate to that folder by typing

cd ~/workspace/src/first_package/first_package

Let us create a source file of the publisher node by typing

gedit publisher.py

Type the following Python code that implements the publisher node

# Python publisher node 
# Author: Aleksandar Haber 
# Date: December 2023

# rclpy is a Python API for communicating and interacting with ROS 2.
# API stands for the Application Programming Interface 
# it is an interface between two programs
import rclpy

# We are sending strings as messages - this similar to ROS1 
from std_msgs.msg import String

# We need the Node class
from rclpy.node import Node


# We create the class PublisherNode that is inherited from rclpy.Node
class PublisherNode(Node):
    def __init__(self):
    	# Here, we call the constructor of the the parent class rclpy.Node
    	# and we specify the name
        super().__init__('node_publisher')
        
        # here we are declaring that our node will publish String messages
        # (from std_msgs.msg).
        # We will use a topic called 'communication_topic', 
        # note that the name of the topic should match the name in the subcriber Python file
        # the last input argument is the queue size for buffering the messages 
        # in our case the queue size is 15
        self.publisher_ = self.create_publisher(String,'communication_topic',15)
        # we create a rate for communicating
        commRate = 1  # in seconds
        
        # and we set the timer
        # the timer will call the function self.callbackFunction defined below every commRate seconds
        self.timer = self.create_timer(commRate, self.callbackFunction)
        # counter 
        self.counter = 0

	# this is the callback function that is executed by the timer
    def callbackFunction(self):
		# create a message
        messagePublisher = String()
        # fill in the message data with a string
        messagePublisher.data = 'Counter value: %d' % self.counter
	    # publish the message to the topic 
        self.publisher_.publish(messagePublisher)
        # publish the message to the logger that will desplay the message in the
        # same window in which the publisher node was started
        self.get_logger().info('Publisher node is publishing:"%s"' % messagePublisher.data)
        # increment the counter
        self.counter=self.counter+1


# main function
def main(args=None):
    # initialize
    rclpy.init(args=args)
	# create the object
    node_publisher = PublisherNode()
    #call the spin function that will spin the node and make sure that callbacks are called
    rclpy.spin(node_publisher)
    node_publisher.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

Then, type:

cd ~/workspace/src/first_package/

Here, we have to edit several files. Among other things, we have to add dependencies that should match imports from our publisher and subscriber Python node files. First, we need to edit the “package.xml” file. Type in the terminal window

gedit package.xml

Fill-in these three tags (with your information):

  <description>This is our first package</description>
  <maintainer email="ml.mecheng@gmail.com">Aleksandar </maintainer>
  <license>New license </license>

Then, immediately after these three tags, add the following

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

By doing this, we add the dependencies. These dependencies should match the imports in the Python source files of the subscriber and publisher nodes. Save and close the file. Next, we need to edit the “setup.py” file. Type

gedit setup.py

and find these three lines

    maintainer='aleksandar',
    maintainer_email='aleksandar@todo.todo',
    description='TODO: Package description',
    license='TODO: License declaration',

Then change these three lines such that they match the corresponding lines from the file  package.xml:

    maintainer='Aleksandar',
    maintainer_email='ml.mecheng@gmail.com',
    description='This is our first package',
    license='New license',

Next, we need to add an entry point for running the code. The entry point is the function main from the Python source file of the publisher. We need to type the following within the “entry_points” dictionary and inside of ‘console_scripts’

    entry_points={
        'console_scripts': [ 'talker = first_package.publisher:main',
        ],
    },

In the line

'talker = first_package.publisher:main'

first_package is the name of the folder containing the source file, and publisher is the name of the Python source file of the publisher node. Next, we need to write the subscriber node. For that purpose, go back to the folder with scripts:

cd ~/workspace/src/first_package/first_package

Let us create a source file of the subscriber node by typing

gedit subscriber.py

Enter the following Python code file that implements the subscriber node

# Python subscriber node 
# Author: Aleksandar Haber 
# Date: December 2023

# rclpy is a Python API for communicating and interacting with ROS 2.
# API stands for the Application Programming Interface 
# it is an interface between two programs
import rclpy

# We are sending strings as messages - this similar to ROS1 
from std_msgs.msg import String

# We need the Node class
from rclpy.node import Node

class SubscriberNode(Node):

    def __init__(self):
# Here, we call the constructor of the the parent class rclpy.Node
# and we specify the name
        super().__init__('node_subscriber')
    	# here we are declaring that our node will receive String messages
        # (from std_msgs.msg).
        

     	# We will use a topic called 'communication_topic', 
       	# note that the name of the topic should match the name in the publisher Python file
       	# We also specify the name of the callback function - it is defined in the sequel
       	
       	# the last input argument is the queue size for buffering the messages 
       	# in our case the queue size is 15    		
        self.subscription=self.create_subscription(String,'communication_topic',self.callbackFunction,15)
	    # prevent unused variable warning
        self.subscription  

	#call back function 
    def callbackFunction(self, message):
        self.get_logger().info('We received: "%s"' % message.data)

# our main function - entry point
def main(args=None):
	# initialize
	rclpy.init(args=args)

	node_subscriber = SubscriberNode()

	rclpy.spin(node_subscriber)

	node_subscriber.destroy_node()
	rclpy.shutdown()


if __name__ == '__main__':
    main()

Then, let us add the entry point for the subscriber node

cd ~/workspace/src/first_package/

gedit setup.py

And add another entry to the entry point dictionary corresponding to the subscriber

    entry_points={
        'console_scripts': [ 'talker = first_package.publisher:main',
        		     'listener = first_package.subscriber:main',	
        ],
    },

STEP 3: Build the Workspace and Run the Nodes

In the last step, we need to build and run the nodes. However, before building the workspace, it is a good idea to check for missing dependencies.  The packages rclpy and std_msgs come as an integral part of ROS2 installation, however, it is still a good idea to double-check for missing dependencies by running:

cd ~/workspace

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

Finally, we are ready to build the workspace and package

cd ~/workspace

colcon build --packages-select first_package

This should build the package. You might see some warnings/errors about the deprecation of some Python packages. These errors do not come from the code that we implemented. For example, you might see this

Starting >>> first_package
--- stderr: first_package                  
/usr/lib/python3/dist-packages/setuptools/command/install.py:34: SetuptoolsDeprecationWarning: setup.py install is deprecated. Use build and pip and other standards-based tools.
  warnings.warn(
---
Finished <<< first_package [0.73s]

Summary: 1 package finished [1.42s]
  1 package had stderr output: first_package

You can ignore this since this comes from the ROS2 binaries and we cannot do anything about this at this point. The code will work independently from this.

Next, let us run the publisher node. For that purpose, open a new terminal and source the overlay workspace

source ~/workspace/install/setup.bash

Then, run the publisher node

ros2 run first_package talker

You will see the following output

[INFO] [1702430337.051641646] [node_publisher]: Publisher node is publishing:"Counter value: 0"
[INFO] [1702430338.042497885] [node_publisher]: Publisher node is publishing:"Counter value: 1"
[INFO] [1702430339.043730069] [node_publisher]: Publisher node is publishing:"Counter value: 2"
[INFO] [1702430340.042412059] [node_publisher]: Publisher node is publishing:"Counter value: 3"
[INFO] [1702430341.042522380] [node_publisher]: Publisher node is publishing:"Counter value: 4"

Then, while this node is running in its terminal, open a new terminal and type the following commands to start the subscriber node:

source ~/workspace/install/setup.bash

ros2 run first_package listener

This will run the subscriber node. The following messages will be printed out in the terminal window

[INFO] [1702430337.042683621] [node_subscriber]: We received: "Counter value: 0"
[INFO] [1702430338.042731895] [node_subscriber]: We received: "Counter value: 1"
[INFO] [1702430339.044485095] [node_subscriber]: We received: "Counter value: 2"
[INFO] [1702430340.042628713] [node_subscriber]: We received: "Counter value: 3"
[INFO] [1702430341.042741022] [node_subscriber]: We received: "Counter value: 4"

This means that both publisher and subscriber nodes are working properly.