
The Heartbeat of Robotics: Understanding ROS2’s Publisher-Subscriber System
Welcome to the exciting world of ROS!. If you’re just starting your journey into robotics software, you’ll quickly encounter a fundamental concept that underpins almost everything: the Publisher-Subscriber (Pub/Sub) communication model. This isn’t just a technical detail; it’s the very heartbeat of how independent components in a robot talk to each other.
Let’s dive into what Pub/Sub is, why it’s so vital in ROS2 and why your first steps in this ecosystem often involve a “Hello World” example built around it. We’ll use your provided Python code as a perfect illustration.
What is the Publisher-Subscriber System?
Imagine a bustling newsroom. You have reporters (publishers) constantly writing articles on various topics (sports, politics, weather). You also have readers (subscriber) who are only interested in specific topics. A reader interested in ‘sports’ doesn’t care about ‘politics’, and a reporter write about ‘weather’ does not need to know who is reading their articles.
This is precisely how Pub/Sub works in ROS2.
- Publishers: Nodes that generate and send data. They “publish” messages to a specific topic.
- Subscribers: Nodes that receives and process data. They “subscribe” to a specific topic and execute a “callback function”.
- Topics: Named channels (like: “temperature” or “robot_positions”) over which messages flow. Publishers send messages to topics and subscribers receive messages from topics.
The key here is decoupling. Publishers and subscribers don’t need to know about each other’s existence. They only need to agree on the topic name and the type of message being exchanged.
Significance in ROS2: The Power of Decentralization.
The Pub/Sub model is absolutely critical for ROS2 because it enables.
- Modularity: You can break down a complex robotic system into many small, manageable node, each responsible for a single task (e.g. a camera driver, a motor controller, a navigation planner).
- Decentralization: No single point of failure. If one node crashes, the others can often continue operating.
- Scalability: Easily add new sensors, actuators or processing algorithms by simply creating new nodes that publish or subscribe to relevant topics, without altering existing code.
- Concurrency: Nodes run independently, often in parallel, utilizing modern multi-core processors effectively.
- Re-usability: Individual nodes can be reused across different robot platforms or applications.
Why is Pub/Sub the “Hello World” of ROS2?
When we learn a new programming language, our first program is often “Hello World” because it’s the simplest way to demonstrate basic output. In ROS2 the Pub/Sub system serves a similar purpose.
- It introduces us to the core communication paradigm.
- It demonstrates how to create nodes, the fundamental building blocks.
- It show how data flow through the ROS2 graph.
- It highlights the use of client libraries (rclpy for Python) and message types.
Mastering Pub/Sub is the gateway to understanding more complex ROS2 features like Services and Actions, which build upon the foundational concept.
Tutorial
We will start this tutorial by creating our first ROS2 workspace, which will be followed by creating our first package. Then we will build a temperature sensor simulator containing a Publisher Node and a Subscriber Node. After that we will configure the package to build and run it.
Create Workspace
Create a Directory name “ROS2_and_RL_Tutorials” and move inside it.
mkdir /Documents/ROS2_and_RL_Tutorials/
cd /Documents/ROS2_and_RL_Tutorials/
Create directory src, docs, scripts, resources, and images.
-p = “parents” flag, which means:
- Can create nested directories in one command
- Create parent directories if they don’t exist
- Don’t error if directory already exists
Move to src
mkdir -p src docs scripts resources/images
cd src
Create a custom python package
ros2 pkg create --build-type ament_python sensor_simulator --dependencies rclpy std_msgs
Simple breakdown:
ros2– The ROS2 command-line tool-
pkg create– Command to create a new package --build-type ament_python– Make it a Python package (not C++)sensor_simulator– Name of your package--dependencies rclpy std_msgs– Automatically add these required libraries:rclpy= ROS2 Python library (needed for all Python nodes)std_msgs= Standard message types (like Float32, String, etc.)
Build and Load ROS2 Package
cd ~/Documents/ROS2_and_RL_Tutorials/
colcon build
source install/setup.bash
cd ~/Documents/ROS2_and_RL_Tutorials/– Go to your workspacecolcon build– Build all ROS2 packages in the workspacesource install/setup.bash– Load the built packages so ROS2 can find them
Create a temperature publisher
cd src/sensor_simulator/sensor_simulator/
nano temperature_publisher.py
This program creates a ROS2 node that simulates a temperature sensor. Every second, it generates a realistic temperature reading by slightly adjusting the previous temperature value (adding or subtracting a small random amount), keeps the temperature within a reasonable range, packages this data into a message, and publishes it to a topic called ‘temperature’ so other nodes can receive and use this information. It’s like a virtual thermometer that continuously broadcasts temperature readings.
Import Section
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float32
import random
#!/usr/bin/env python3– Tells the system to run this file with Python 3import rclpy– Imports the ROS2 Python library for all ROS2 functionalityfrom rclpy.node import Node– Imports the Node base class to create ROS2 nodesfrom std_msgs.msg import Float32– Imports Float32 message type to send decimal numbersimport random– Imports Python’s random library to simulate temperature fluctuations
Class Definition
class TemperatureSensor(Node):
class TemperaturSensor(Node):– Creates a new class inheriting from Node, giving it all ROS2 node capabilities.
Constructor (init method)
def __init__(self):
super().__init__('temperature_sensor')
#Create publisher on "temperature" topic
self.publisher_ = self.create_publisher(Float32,'temperature',10)
#Set up time to publish every 1 second
self.timer = self.create_timer(1.0,self.publish_temperature)
#initial temperature
self.temperature = 22.0
self.get_logger().info("Temperature Sensor Node Started")
def __init__(self):– Constructor that runs when creating a TemperaturSensor objectsuper().__init__('temperature_sensor')– Initializes the parent Node class and names this node ‘temperature_sensor’self.publisher_ = self.create_publisher(Float32,'temperature',10)– Creates a publisher objectFloat32– Message type to publish (floating-point numbers)'temperature'– Topic name where messages will be sent10– QoS queue size (keeps last 10 messages if subscribers are slow)
self.timer = self.create_timer(1.0, self.publish_temperature)– Creates a timer that calls a function repeatedly1.0– Time interval in seconds (calls every 1 second)self.publish_temperature– Function to call when timer triggers
self.temperature = 22.0– Sets initial temperature to 22 degrees Celsius (room temperature)self.get_logger().info(...)– Logs a startup message to the terminal
Publishing Function
def publish_temperature(self):
#simulate temperature fluctuation
self.temperature += random.uniform(-0.5,0.5)
#keep temperature in realistic range
self.temperature = max(10.2,min(28.0,self.temperature))
#create and publish message
msg = Float32()
msg.data = self.temperature
self.publisher_.publish(msg)
self.get_logger().info(f'Publishing: {self.temperature:.2f}C')
def publish_temperature(self):– Function called automatically every second by the timerself.temperature += random.uniform(-0.5, 0.5)– Simulates realistic temperature changesrandom.uniform(-0.5, 0.5)– Generates a random number between -0.5 and +0.5+=– Adds this random value to current temperature (making it go up or down slightly)
self.temperature = max(10.2, min(28.0, self.temperature))– Constrains temperature to realistic boundsmin(28.0, self.temperature)– If temperature exceeds 28°C, cap it at 28°Cmax(10.2, ...)– If temperature drops below 10.2°C, set it to 10.2°C (note: likely typo, probably meant 18.0°C)- Result: Temperature stays between 10.2°C and 28°C
msg = Float32()– Creates a new Float32 message objectmsg.data = self.temperature– Puts the temperature value into the message’s data fieldself.publisher_.publish(msg)– Sends the message to all subscribers on the ‘temperature’ topicself.get_logger().info(f'Publishing: {self.temperature:.2f}C')– Logs what was published{self.temperature:.2f}– Formats temperature to 2 decimal places
Main Function
def main(args=None):
rclpy.init(args=args)
node = TemperaturSensor()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
node.destroy_node()
rclpy.shutdown()
def main(args=None):– Entry point functionrclpy.init(args=args)– Initializes ROS2 (must be called before creating nodes)node = TemperaturSensor()– Creates instance of the sensor node (triggers__init__)try:– Starts error handling blockrclpy.spin(node)– Keeps node running and processes timer callbacks (infinite loop that callspublish_temperatureevery second)
except KeyboardInterrupt:– Catches Ctrl+Cpass– Does nothing, allows graceful exit
node.destroy_node()– Cleans up the node resources (good practice)rclpy.shutdown()– Shuts down ROS2 cleanly
Script Entry Point
if __name__ == '__main__':
main()
if __name__ == '__main__':– Checks if file is run directly (not imported)main()– Starts the program
How Data Flows Through This Code
- Startup:
main()→ initializes ROS2 → createsTemperaturSensornode → sets up publisher and timer - Timer triggers (every 1 second):
publish_temperature()is called automatically- Temperature is adjusted randomly
- Temperature is clamped to range
- Message is created and published
- Log message displayed
- Repeat: Timer keeps triggering every second
- Shutdown: User presses Ctrl+C → node cleaned up → ROS2 shutdown
The Complete Publisher Code
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float32
import random
class TemperaturSensor(Node):
def __init__(self):
super().__init__('temperature_sensor')
#Create publisher on "temperature" topic
self.publisher_ = self.create_publisher(Float32,'temperature',10)
#Set up time to publish every 1 second
self.timer = self.create_timer(1.0,self.publish_temperature)
#initial temperature
self.temperature = 22.0
self .get_logger().info("Temperature Sensor Node Started")
def publish_temperature(self):
#simulate temperature fluctuation
self.temperature += random.uniform(-0.5,0.5)
#keep temperature inrealistic range
self.temperature =max(10.2,min(28.0,self.temperature))
#create and publish message
msg = Float32()
msg.data = self.temperature
self.publisher_.publish(msg)
self.get_logger().info(f'Publishing: {self.temperature:.2f}C')
def main(args=None):
rclpy.init(args=args)
node = TemperaturSensor()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Create a Temperature Subscriber
The following program creates a ROS2 node that acts as a temperature monitoring system. It listens to temperature data being published on a topic called ‘temperature’, receives each temperature reading as it arrives, evaluates whether the temperature is too high, too low, or normal, and then logs an alert message with the current temperature and its status. Think of it as a thermometer display that continuously watches incoming temperature data and warns you when things get too hot or cold.
Imports Section
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float32
#!/usr/bin/env python3– Shebang line that tells the system to run this file with Python 3import rclpy– Imports the ROS2 Python client library (needed for all ROS2 functionality)from rclpy.node import Node– Imports the Node class, which is the base class for creating ROS2 nodesfrom std_msgs.msg import Float32– Imports the Float32 message type to receive decimal number data
Class Definition
class TemperatureMonitor(Node):
class TemperatureMonitor(Node)– Creates a new class that inherits from the ROS2 Node class, giving it all the capabilities of a ROS2 node (publishing, subscribing, logging, etc.)
Constructor (init method)
def __init__(self):
super().__init__('temperature_monitor')
#Create subscriber to 'temperature' topic
self.subscription = self.create_subscription(
Float32,
'temperature',
self.temperature_callback,
10
)
self.get_logger().info('Temperature Monitor Node Started')
def __init__(self):– Constructor method that runs when you create a TemperatureMonitor objectsuper().__init__('temperature_monitor')– Calls the parent Node class constructor and names this node ‘temperature_monitor’self.subscription = self.create_subscription(...)– Creates a subscriber object that listens for messagesFloat32– The type of message to expect (floating-point numbers)'temperature'– The name of the topic to listen toself.temperature_callback– The function to call when a message arrives10– Quality of Service (QoS) queue size – keeps the last 10 messages if processing is slow
self.get_logger().info(...)– Logs an informational message to the terminal when the node starts
Callback Function
def temperature_callback(self, msg):
temp = msg.data
#Simple alert system
if temp > 26.0:
status = "High"
elif temp < 20.0:
status = "Low"
else:
status = "Normal"
self.get_logger().info(f'Temperature: {temp:.2f}C - Status:{status}')
def temperature_callback(self, msg):– Function that gets called automatically every time a new temperature message arrivestemp = msg.data– Extracts the actual temperature value from the message (Float32 messages have a.datafield)if temp > 26.0:– Checks if temperature is above 26 degreesstatus = "High"– Sets status to “High” if temperature exceeds threshold
elif temp < 20.0:– Checks if temperature is below 20 degreesstatus = "Low"– Sets status to “Low” if temperature is too cold
else:– If temperature is between 20 and 26status = "Normal"– Sets status to “Normal” for comfortable range
self.get_logger().info(f'...')– Logs the temperature and status{temp:.2f}– Formats temperature to 2 decimal placesf'...'– f-string formatting to insert variables into the string
Main Function
def main(args=None):
rclpy.init(args=args)
node = TemperatureMonitor()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
def main(args=None):– Entry point function that starts the programrclpy.init(args=args)– Initializes the ROS2 Python client library (must be called before creating nodes)node = TemperatureMonitor()– Creates an instance of the TemperatureMonitor class (this triggers__init__)try:– Begins a try-except block for error handlingrclpy.spin(node)– Keeps the node running and processing callbacks indefinitely (like an infinite loop that responds to incoming messages)
except KeyboardInterrupt:– Catches Ctrl+C keyboard interruptpass– Does nothing, allowing clean exit when user presses Ctrl+C
Script Entry Point
if __name__ == "__main__":
main()
if __name__ == "__main__":– Checks if this file is being run directly (not imported as a module)-
main()– Calls the main function to start the program
How Data Flows Through This Code
- Startup:
main()→ initializes ROS2 → createsTemperatureMonitornode → sets up subscriber - Waiting:
rclpy.spin(node)keeps the program alive, waiting for messages - Message arrives: When publisher sends temperature data →
temperature_callback()is triggered automatically - Processing: Callback extracts temperature → evaluates status → logs result
- Repeat: Goes back to waiting for next message
- Shutdown: User presses Ctrl+C → exits cleanly
This subscriber node is passive – it just waits and reacts to data published by the temperature_publisher node!
The Complete Subscriber Code
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import Float32
class TemperatureMonitor(Node):
def __init__(self):
super().__init__('temperature_monitor')
#Create subscriber to 'temperature' topic
self .subscription = self.create_subscription(
Float32,
'temperature',
self.temperature_callback,
10
)
self.get_logger().info('Temperature Monitor Node Started')
def temperature_callback(self,msg):
temp = msg.data
#Simple alert system
if temp > 26.0:
status = "High"
elif temp < 20.0:
status = "Low"
else:
status= "Normal"
self.get_logger().info(f'Temperature: {temp:.2f}C - Status:{status}')
def main(args=None):
rclpy.init(args=args)
node = TemperatureMonitor()
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
if __name__ == "__main__":
main()
Key Differences Between Publisher and Subscriber Code
| Publisher | Subscriber |
| Sends data | Receives data |
Uses create_publisher() | Uses create_subscription() |
| Uses timer (proactive – generates data) | Uses callback (reactive – waits for data) |
Calls publish() to send | Callback triggered automatically when data arrives |
| Generates simulated data | Processes received data |
Make Python code executable
chmod +x temperature_publisher.py
chmod +x temperature_subscriber.py
chmod +x temperature_publisher.pychmod= “change mode” – modifies file permissions+x= adds “execute” permission, making the file runnable as a program (allows you to run./temperature_publisher.pydirectly instead ofpython3 temperature_publisher.py)
- chmod +x temperature_subscriber.py
- Does the same thing for the subscriber file
After this, both Python scripts can be executed directly by ROS2’s ros2 run command
Edit the setup file
Set the entry points as follows.
cd ~/Documents/ROS2_and_RL_Tutorials/src/sensor_simulator/
nano setup.py
from setuptools import find_packages, setup
package_name = 'sensor_simulator'
setup(
name=package_name,
version='0.0.0',
packages=find_packages(exclude=['test']), data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='janak',
maintainer_email='janak.klal@gmail.com',
description='TODO: Package description',
license='TODO: License declaration',
extras_require={
'test': [
'pytest',
],
},
entry_points={
'console_scripts': [
'temperature_publisher = sensor_simulator.temperature_publisher:main',
'temperature_subscriber = sensor_simulator.temperature_subscriber:main',
],
},
)
Function of setup.py
setup.py is the configuration file that tells Python and ROS2 how to install and run your package. It defines:
- Package metadata (name, version, author)
- What files to include
- Dependencies needed
- Most importantly: Which Python scripts become executable commands
Think of it as the “instruction manual” for installing your package – it tells colcon build what to do with your code.
What are entry points?
entry_points create command-line executables from your Python functions. They’re the bridge between typing ros2 run and your actual Python code.
'temperature_publisher = sensor_simulator.temperature_publisher:main'
temperature_publisher= The command you type in terminal (ros2 run sensor_simulator temperature_publisher)-
sensor_simulator.temperature_publisher= The Python file path (sensor_simulator/temperature_publisher.py) :main= The specific function to run (themain()function in that file)
Build, Load and Run …
cd ~/Documents/ROS2_and_RL_Tutorials/
- Changes your current directory to the workspace folder where your ROS2 project lives
colcon build --packages-select sensor_simulator
- Compiles/builds only the sensor_simulator package (not all packages in workspace), creating executables from your Python code
source install/setup.bash
- Loads the built package into your terminal session so ROS2 knows where to find your executables
ros2 run sensor_simulator temperature_publisher
- Runs the temperature_publisher executable from the sensor_simulator package, starting your temperature sensor node
In another Terminal
ros2 run sensor_simulator temperature_subscriber
- Runs the temperature_subscriber
Output
