Publisher/Subscriber
This page walks through a complete, minimal Python publisher and subscriber inside the my_ros2_tutorial package created in Workspace and Package. By the end you will have two nodes talking to each other over a topic, built and run entirely by you.
Overview
The publisher node (simple_publisher) sends a std_msgs/msg/String message on the /tutorial_chatter topic once per second. The subscriber node (simple_subscriber) listens on the same topic and logs whatever it receives. This is the topic communication pattern from ROS 2 Concepts in its smallest possible working form.
The code uses the relative topic name tutorial_chatter. With the nodes running in the root namespace as shown here, ROS 2 resolves it to /tutorial_chatter.
Why It Matters
Many ROS 2 nodes build on the same basic pattern shown here: a node, communication interfaces such as publishers or subscriptions, callbacks, and an executor that processes those callbacks. Once this pattern is familiar, reading and writing more complex nodes becomes much easier.
Prerequisites
- Completed Workspace and Package: a sourced ROS 2 environment and the
my_ros2_tutorialpackage created with--dependencies rclpy std_msgs. - A text editor available on the machine or container running ROS 2.
Core Concepts
- Node: a Python class that inherits from
rclpy.node.Node. - Publisher: created with
self.create_publisher(MsgType, topic_name, qos); call.publish(msg)to send. - Subscriber: created with
self.create_subscription(MsgType, topic_name, callback, qos); ROS 2 callscallbackwhenever a message arrives. - Timer:
self.create_timer(period_sec, callback)callscallbackon a fixed interval — used here to publish once per second. rclpy.spin(node): blocks and processes callbacks (timers, subscriptions) until the node is shut down (e.g.,Ctrl+C).- QoS depth (
10below): with the default keep-last history policy, this specifies how many messages may be retained in the queue. Publisher and subscriber QoS settings do not need to be identical, but they must be compatible for communication to occur.
Hands-on Steps
1. Create the Publisher File
Create ~/ros2_ws/src/my_ros2_tutorial/my_ros2_tutorial/simple_publisher.py:
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class SimplePublisher(Node):
def __init__(self):
super().__init__('simple_publisher')
self.publisher_ = self.create_publisher(String, 'tutorial_chatter', 10)
self.timer_period = 1.0 # seconds
self.timer = self.create_timer(self.timer_period, self.timer_callback)
self.count = 0
def timer_callback(self):
msg = String()
msg.data = f'Hello ROS 2: {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)
node = SimplePublisher()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
2. Create the Subscriber File
Create ~/ros2_ws/src/my_ros2_tutorial/my_ros2_tutorial/simple_subscriber.py:
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class SimpleSubscriber(Node):
def __init__(self):
super().__init__('simple_subscriber')
self.subscription = self.create_subscription(
String,
'tutorial_chatter',
self.listener_callback,
10)
self.subscription # prevent unused variable warning
def listener_callback(self, msg):
self.get_logger().info(f'Received: "{msg.data}"')
def main(args=None):
rclpy.init(args=args)
node = SimpleSubscriber()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
3. Register Entry Points in setup.py
Open ~/ros2_ws/src/my_ros2_tutorial/setup.py and edit the entry_points dictionary so it looks like this (add the two lines inside console_scripts):
entry_points={
'console_scripts': [
'simple_publisher = my_ros2_tutorial.simple_publisher:main',
'simple_subscriber = my_ros2_tutorial.simple_subscriber:main',
],
},
This is what makes ros2 run my_ros2_tutorial simple_publisher resolve to the main() function above. No changes to package.xml are needed since rclpy and std_msgs were already declared as dependencies when the package was created.
4. Build and Source
cd ~/ros2_ws
colcon build --packages-select my_ros2_tutorial --symlink-install
source install/setup.bash
5. Run Both Nodes
In one sourced terminal:
ros2 run my_ros2_tutorial simple_publisher
In a second sourced terminal:
ros2 run my_ros2_tutorial simple_subscriber
Expected Result
The publisher terminal prints one line per second:
[INFO] [simple_publisher]: Publishing: "Hello ROS 2: 0"
[INFO] [simple_publisher]: Publishing: "Hello ROS 2: 1"
[INFO] [simple_publisher]: Publishing: "Hello ROS 2: 2"
The subscriber terminal prints a matching line for each message received:
[INFO] [simple_subscriber]: Received: "Hello ROS 2: 0"
[INFO] [simple_subscriber]: Received: "Hello ROS 2: 1"
[INFO] [simple_subscriber]: Received: "Hello ROS 2: 2"
You can also inspect the topic from a third sourced terminal.
To view the messages:
ros2 topic echo /tutorial_chatter
Stop it with Ctrl+C, then check the publish rate:
ros2 topic hz /tutorial_chatter
Stop each node with Ctrl+C when finished.
Useful Commands
# Confirm both nodes are visible in the graph
ros2 node list
# Inspect the topic that connects them
ros2 topic info /tutorial_chatter -v
ros2 interface show std_msgs/msg/String
# Visualize the connection
ros2 run rqt_graph rqt_graph
Common Problems
ros2 run my_ros2_tutorial simple_publisherfails with "No executable found" — the entry point was not added correctly insetup.py, or the workspace was not rebuilt after editing it. Re-check theentry_pointsblock, then re-runcolcon build --packages-select my_ros2_tutorial.- Subscriber prints nothing — confirm the publisher is still running, confirm both terminals sourced the same overlay and share the same
ROS_DOMAIN_ID(see ROS 2 Domain ID), and checkros2 topic info /tutorial_chatter -vshows both a publisher and a subscriber connected. ModuleNotFoundError: No module named 'my_ros2_tutorial'— the overlay was not sourced in this terminal after building. Runsource ~/ros2_ws/install/setup.bashagain.- Editing the
.pyfile has no effect on the running node — the workspace was built without--symlink-install, or the node was not restarted after the edit. Stop the node (Ctrl+C), rebuild if needed, and run it again. - General discovery issues (nothing shows up anywhere) — see Troubleshooting for the full checklist.
Key Takeaways
- A minimal ROS 2 node is a class, a publisher or subscriber, a callback, and
rclpy.spin(). create_timerdrives periodic publishing;create_subscriptiondrives reactive handling of incoming messages.- Entry points in
setup.pyare what makeros2 run <package> <executable>work — they must match the file and function exactly. --symlink-installavoids rebuilding for every Python source edit while iterating.
Next
Continue to Services and Actions to add request/response and long-running-goal communication to the same package, or to Parameters and Launch to make the publish rate configurable instead of hardcoded.