Creating Publisher and Subscriber Nodes in ROS 2 with Python

Introduction to ROS2

Last updated: December 22, 2024

1. Introduction

In this lesson, we will learn how to create ROS 2 publisher and subscriber nodes using Python. We will:

Complete codetalker_listener_py on Edreate GitHub

1a. What is a ROS 2 Package?

A ROS 2 package is a structured directory that contains nodes, libraries, launch files, and configuration files. It acts as a modular component of your ROS 2 project. The package is defined by a package.xml file and typically built with colcon. For Python-based ROS 2 nodes, we often use the ament_python build type.

1b. What is a Publisher?

A publisher is a ROS 2 node element that sends messages over a specified topic. Other nodes can subscribe to that topic to receive data. Publishers are created using create_publisher(), where you specify the message type, topic name, and queue size.

1c. What is a Subscriber?

A subscriber is a ROS 2 node element that listens for messages on a specified topic. It uses a callback function to process incoming data. Subscribers are created using create_subscription(), providing the message type, topic name, callback, and queue size.


2. Step-by-Step Implementation

2a. Prerequisites

Make sure you have:

Install necessary dependencies if you haven’t already:

sudo apt update
sudo apt install python3-colcon-common-extensions

2b. Set Up the Workspace

Create a new workspace and source ROS 2:

mkdir -p ~/ros2_ws/src
cd ~/ros2_ws
source /opt/ros/humble/setup.bash

Create a new Python-based ROS 2 package named talker_listener:

cd src
ros2 pkg create --build-type ament_python --node-name talker talker_listener_py
Terminal Output Example:
going to create a new package
package name: talker_listener_py
destination directory: /home/edreate/Desktop/ROS2Introduction/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['edreate <edreate@todo.todo>']
licenses: ['TODO: License declaration']
build type: ament_python
dependencies: []
node_name: talker
creating folder ./talker_listener_py
creating ./talker_listener_py/package.xml
creating source folder
creating folder ./talker_listener_py/talker_listener_py
creating ./talker_listener_py/setup.py
creating ./talker_listener_py/setup.cfg
creating folder ./talker_listener_py/resource
creating ./talker_listener_py/resource/talker_listener_py
creating ./talker_listener_py/talker_listener_py/__init__.py
creating folder ./talker_listener_py/test
creating ./talker_listener_py/test/test_copyright.py
creating ./talker_listener_py/test/test_flake8.py
creating ./talker_listener_py/test/test_pep257.py
creating ./talker_listener_py/talker_listener_py/talker.py

[WARNING]: Unknown license 'TODO: License declaration'.  This has been set in the package.xml, but no LICENSE file has been created.
It is recommended to use one of the ament license identitifers:
Apache-2.0
BSL-1.0
BSD-2.0
BSD-2-Clause
BSD-3-Clause
GPL-3.0-only
LGPL-3.0-only
MIT
MIT-0

You now have a talker_listener package with a basic structure. You can check this by running tree commannd in the terminal.

sudo apt install tree

# In src folder run
tree

Output:

.
└── talker_listener_py
    ├── package.xml
    ├── resource
    │   └── talker_listener_py
    ├── setup.cfg
    ├── setup.py
    ├── talker_listener_py
    │   ├── __init__.py
    │   └── talker.py
    └── test
        ├── test_copyright.py
        ├── test_flake8.py
        └── test_pep257.py

2c. Create the Talker (Publisher)

Navigate into the talker_listener directory and create a file named talker.py:

talker.py:

import rclpy
from rclpy.node import Node

from std_msgs.msg import String


class Talker(Node):
    def __init__(self):
        super().__init__("talker_py")

        self.publisher = self.create_publisher(String, "talker", 10)

        self.timer = self.create_timer(1.0, self.talker_callback)

        self.talker_count = 0

    def talker_callback(self):
        msg = String()
        msg.data = f"Hello from talker: {self.talker_count}"

        self.publisher.publish(msg)

        self.get_logger().info(f"Published message: {msg.data}")

        self.talker_count += 1


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

    talker = Talker()
    rclpy.spin(talker)


if __name__ == "__main__":
    main()

Update setup.py

Open setup.py and update it to include the talker entry point:

from setuptools import find_packages, setup

package_name = "talker_listener_py"

setup(
    name=package_name,
    version="0.0.1",
    packages=find_packages(exclude=["test"]),
    data_files=[
        (
            "share/ament_index/resource_index/packages",
            ["resource/" + package_name],
        ),
        (
            "share/" + package_name,
            ["package.xml"],
        ),
        # Launch file
        (
            "share/talker_listener_py/launch",
            ["launch/talker_listener_py.launch.py"],
        ),
    ],
    install_requires=["setuptools"],
    zip_safe=True,
    maintainer="edreate",
    maintainer_email="edreate.dev@gmail.com",
    description="Basic talker listener node in python.",
    license="MIT",
    tests_require=["pytest"],
    entry_points={
        "console_scripts": [
            "talker = talker_listener_py.talker:main",
            "listener = talker_listener_py.listener:main",
        ],
    },
)

2d. Build and Run the Talker Node

From the root of your workspace:

cd ~/ros2_ws
colcon build --symlink-install

Terminal Output Example:

Starting >>> talker_listener_py
Finished <<< talker_listener_py [1.81s]          

Summary: 1 package finished [2.30s]

Source the newly built setup files:

source install/setup.bash

Now run the talker node:

ros2 run talker_listener_py talker

Expected Terminal Output:

[INFO] [1734783147.274020622] [talker_py]: Published message: Hello from talker: 0
[INFO] [1734783148.251519650] [talker_py]: Published message: Hello from talker: 1
[INFO] [1734783149.251430572] [talker_py]: Published message: Hello from talker: 2
[INFO] [1734783150.251331157] [talker_py]: Published message: Hello from talker: 3

Leave this running, or open a new terminal for the next steps.


3. Create the Listener (Subscriber)

In the talker_listener package, create listener.py:

listener.py:

import rclpy
from rclpy.node import Node

from std_msgs.msg import String


class Listener(Node):
    def __init__(self):
        super().__init__("listener_py")

        self.subscriber = self.create_subscription(String, "talker", self.listener_callback, 10)

    def listener_callback(self, msg: String):
        self.get_logger().info(f"Listener received messaged: {msg.data}")


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

    listener = Listener()
    rclpy.spin(listener)


if __name__ == "__main__":
    main()

Add Listener to setup.py

Update setup.py to include the listener node:

entry_points={
        'console_scripts': [
            'talker = talker_listener_py.talker:main',
            'listener = talker_listener_py.listener:main'
        ],
    }

Rebuild the package:

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

Terminal Output Example:

Starting >>> talker_listener_py
Finished <<< talker_listener_py [1.76s]          

Summary: 1 package finished [2.27s]

3a. Run the Listener Node

In a new terminal (with the workspace sourced):

ros2 run talker_listener_py listener

Expected Terminal Output:

[INFO] [1734783204.283136954] [listener_py]: Listener received messaged: Hello from talker: 0
[INFO] [1734783205.251829147] [listener_py]: Listener received messaged: Hello from talker: 1
[INFO] [1734783206.251647974] [listener_py]: Listener received messaged: Hello from talker: 2

4. ROS 2 CLI Tools

You can verify your running nodes and topics using the ROS 2 CLI:

List active nodes:

ros2 node list

Expected Output:

/listener_py
/talker_py

List available topics:

ros2 topic list

Expected Output:

/parameter_events
/rosout
/talker

Echo messages from the talker topic (in another terminal):

ros2 topic echo /talker

Expected Output:

data: 'Hello from talker: 1'
---
data: 'Hello from talker: 2'
---

These tools help verify that nodes and topics are working as expected.


5. Summary

In this lesson, we:

Complete code: talker_listener_py on Edreate GitHub

This fundamental setup of publishers and subscribers forms the building block for more complex robotic applications in ROS 2.

Next Lesson