ROS 2 Foundations · Lesson 4 of 6

Lesson 04

Topics: Publisher & Subscriber

This is the lesson everything has been building toward. You'll write two real nodes in Python — one that publishes messages and one that receives them — then run them and watch your own code talk over the graph.

Meet rclpy

You write ROS 2 nodes in Python using a library called rclpy (ROS Client Library for Python). The pattern for almost every node is the same four steps:

  1. Create a class that inherits from Node.
  2. In its constructor, set up publishers, subscribers, and timers.
  3. Write the logic (a timer callback that publishes, or a subscriber callback that reacts).
  4. A main() that starts ROS, spins the node, and cleans up.

Write the publisher

Inside your package from Lesson 2, create a new file. It publishes a counting message to the topic /robot_status twice per second.

~/ros2_ws/src/my_robot/my_robot/status_publisher.py

import rclpy
from rclpy.node import Node
from std_msgs.msg import String


class StatusPublisher(Node):
    def __init__(self):
        super().__init__('status_publisher')      # the node's name
        # create_publisher(message_type, topic_name, queue_size)
        self.publisher = self.create_publisher(String, 'robot_status', 10)
        self.count = 0
        # call self.tick() every 0.5 seconds
        self.timer = self.create_timer(0.5, self.tick)
        self.get_logger().info('status_publisher has started')

    def tick(self):
        msg = String()
        msg.data = f'Robot OK — heartbeat {self.count}'
        self.publisher.publish(msg)
        self.get_logger().info(f'Publishing: "{msg.data}"')
        self.count += 1


def main(args=None):
    rclpy.init(args=args)          # start up ROS 2
    node = StatusPublisher()
    rclpy.spin(node)               # keep running, firing callbacks
    node.destroy_node()
    rclpy.shutdown()               # clean up


if __name__ == '__main__':
    main()
Read the key lines

create_publisher(String, 'robot_status', 10) says "I will send String messages on the robot_status topic, and buffer up to 10 if the network is slow." create_timer(0.5, self.tick) runs tick() twice a second. spin() is what keeps the node alive and firing those callbacks.

Write the subscriber

Now a second node that listens on the same topic and reacts to every message.

~/ros2_ws/src/my_robot/my_robot/status_listener.py

import rclpy
from rclpy.node import Node
from std_msgs.msg import String


class StatusListener(Node):
    def __init__(self):
        super().__init__('status_listener')
        # create_subscription(type, topic, callback, queue_size)
        self.subscription = self.create_subscription(
            String, 'robot_status', self.on_message, 10)
        self.get_logger().info('status_listener is waiting for messages')

    def on_message(self, msg):
        # this runs every time a message arrives on robot_status
        self.get_logger().info(f'Received: "{msg.data}"')


def main(args=None):
    rclpy.init(args=args)
    node = StatusListener()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

Notice the symmetry: the publisher used a timer callback to send; the subscriber uses a message callback to receive. Both must use the same message type and topic name to connect — String on robot_status.

Tell ROS 2 how to run them

ROS 2 needs to know these files are runnable programs. Open setup.py and add both to the entry_points section:

~/ros2_ws/src/my_robot/setup.py (excerpt)

    entry_points={
        'console_scripts': [
            'status_publisher = my_robot.status_publisher:main',
            'status_listener = my_robot.status_listener:main',
        ],
    },

Each line reads: <command_name> = <package>.<file>:<function>. Also confirm std_msgs and rclpy are listed as dependencies in package.xml:

<depend>rclpy</depend>
<depend>std_msgs</depend>

Build and run

Terminal — build once

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

Now open two terminals (source install/setup.bash in each) and run one node in each:

Terminal 1 — publisher

ros2 run my_robot status_publisher

Terminal 2 — subscriber

ros2 run my_robot status_listener

The publisher logs Publishing: "Robot OK — heartbeat 3" and the listener logs Received: "Robot OK — heartbeat 3". You wrote both sides of a robot conversation. Open a third terminal and run rqt_graph to see /status_publisher → /robot_status → /status_listener.

Exercises — make it yours
  1. Change the publisher's timer to fire 5 times per second. What number goes in create_timer?
  2. Make the listener print ALERT whenever the heartbeat number is a multiple of 10. (Hint: you'll need to send a number — try switching to the std_msgs/msg/Int32 message type, whose field is data as an integer.)
  3. Run a second copy of the listener in a new terminal. Do both receive every message? Why? (Re-read the one-to-many idea from Lesson 1.)

Key takeaways