Python
The Heartbeat of Robotics: Understanding ROS2’s Publisher-Subscriber System

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.

  1. 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).
  2. Decentralization: No single point of failure. If one node crashes, the others can often continue operating.
  3. 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.
  4. Concurrency: Nodes run independently, often in parallel, utilizing modern multi-core processors effectively.
  5. 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 workspace
  • colcon build – Build all ROS2 packages in the workspace
  • source 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 3
  • import rclpy – Imports the ROS2 Python library for all ROS2 functionality
  • from rclpy.node import Node – Imports the Node base class to create ROS2 nodes
  • from std_msgs.msg import Float32 – Imports Float32 message type to send decimal numbers
  • import 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 object
  • super().__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 object
    • Float32 – Message type to publish (floating-point numbers)
    • 'temperature' – Topic name where messages will be sent
    • 10 – 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 repeatedly
    • 1.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 timer
  • self.temperature += random.uniform(-0.5, 0.5) – Simulates realistic temperature changes
    • random.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 bounds
    • min(28.0, self.temperature) – If temperature exceeds 28°C, cap it at 28°C
    • max(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 object
  • msg.data = self.temperature – Puts the temperature value into the message’s data field
  • self.publisher_.publish(msg) – Sends the message to all subscribers on the ‘temperature’ topic
  • self.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 function
  • rclpy.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 block
    • rclpy.spin(node) – Keeps node running and processes timer callbacks (infinite loop that calls publish_temperature every second)
  • except KeyboardInterrupt: – Catches Ctrl+C
    • pass – 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 → creates TemperaturSensor node → 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 3
  • import 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 nodes
  • from 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 object
  • super().__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 messages
    • Float32 – The type of message to expect (floating-point numbers)
    • 'temperature' – The name of the topic to listen to
    • self.temperature_callback – The function to call when a message arrives
    • 10 – 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 arrives
  • temp = msg.data – Extracts the actual temperature value from the message (Float32 messages have a .data field)
  • if temp > 26.0: – Checks if temperature is above 26 degrees
    • status = "High" – Sets status to “High” if temperature exceeds threshold
  • elif temp < 20.0: – Checks if temperature is below 20 degrees
    • status = "Low" – Sets status to “Low” if temperature is too cold
  • else: – If temperature is between 20 and 26
    • status = "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 places
    • f'...' – 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 program
  • rclpy.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 handling
    • rclpy.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 interrupt
    • pass – 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

  1. Startup:main() → initializes ROS2 → creates TemperatureMonitor node → sets up subscriber
  2. Waiting:rclpy.spin(node) keeps the program alive, waiting for messages
  3. Message arrives: When publisher sends temperature data → temperature_callback() is triggered automatically
  4. Processing: Callback extracts temperature → evaluates status → logs result
  5. Repeat: Goes back to waiting for next message
  6. 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

PublisherSubscriber
Sends dataReceives data
Uses create_publisher()Uses create_subscription()
Uses timer (proactive – generates data)Uses callback (reactive – waits for data)
Calls publish() to sendCallback triggered automatically when data arrives
Generates simulated dataProcesses received data

Make Python code executable

chmod +x temperature_publisher.py 
chmod +x temperature_subscriber.py
  • chmod +x temperature_publisher.py
    • chmod = “change mode” – modifies file permissions
    • +x = adds “execute” permission, making the file runnable as a program (allows you to run ./temperature_publisher.py directly instead of python3 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 (the main() 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

Tags :