Nodes and Topics
Learning Objectives
By the end of this chapter, you will be able to:
- Create and manage ROS 2 nodes in both Python and C++
- Understand the publish-subscribe communication pattern
- Design effective topic architectures for robot systems
- Implement robust message passing between nodes
- Use tools to monitor and debug topic communication
Prerequisites
- Completion of Chapter 1: Introduction to ROS 2
- Basic Python or C++ programming knowledge
- Understanding of object-oriented programming concepts
Understanding Nodes
In ROS 2, a node is the fundamental building block of a robot application. A node is a process that performs computation and communicates with other nodes through topics, services, and actions.
Node Lifecycle
ROS 2 nodes have a well-defined lifecycle that includes several states:
- Unconfigured: The node is created but not yet configured
- Inactive: The node is configured but not yet activated
- Active: The node is running and performing its tasks
- Finalized: The node has been shut down and cleaned up
This lifecycle allows for more robust and predictable system behavior, especially in complex robotic applications.
Creating a Node in Python
Here's a basic example of creating a ROS 2 node in Python:
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MinimalPublisher(Node):
def __init__(self):
super().__init__('minimal_publisher')
self.publisher_ = self.create_publisher(String, 'topic', 10)
timer_period = 0.5 # seconds
self.timer = self.create_timer(timer_period, self.timer_callback)
self.i = 0
def timer_callback(self):
msg = String()
msg.data = 'Hello World: %d' % self.i
self.publisher_.publish(msg)
self.get_logger().info('Publishing: "%s"' % msg.data)
self.i += 1
def main(args=None):
rclpy.init(args=args)
minimal_publisher = MinimalPublisher()
rclpy.spin(minimal_publisher)
minimal_publisher.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Creating a Node in C++
Here's the equivalent node in C++:
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
class MinimalPublisher : public rclcpp::Node
{
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0)
{
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
timer_ = this->create_wall_timer(
500ms, std::bind(&MinimalPublisher::timer_callback, this));
}
private:
void timer_callback()
{
auto message = std_msgs::msg::String();
message.data = "Hello World: " + std::to_string(count_++);
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
publisher_->publish(message);
}
rclcpp::TimerBase::SharedPtr timer_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
size_t count_;
};
int main(int argc, char * argv[])
{
rclpy::init(argc, argv);
rclpy::spin(std::make_shared<MinimalPublisher>());
rclpy::shutdown();
return 0;
}
Topics: The Publish-Subscribe Pattern
Topics are the primary method of communication in ROS 2. They use a publish-subscribe pattern where nodes can publish messages to a topic and other nodes can subscribe to that topic to receive the messages.
Topic Characteristics
- Asynchronous: Publishers and subscribers don't need to run at the same time
- Many-to-many: Multiple publishers can publish to the same topic, and multiple subscribers can subscribe to the same topic
- Typed: Each topic has a specific message type that all publishers and subscribers must use
- Named: Topics have hierarchical names (e.g.,
/arm/joint_states)
Quality of Service (QoS) Settings
ROS 2 provides Quality of Service (QoS) settings that allow you to configure the reliability, durability, and performance characteristics of topic communication:
from rclpy.qos import QoSProfile, ReliabilityPolicy, DurabilityPolicy
# Create a QoS profile for reliable communication
qos_profile = QoSProfile(
depth=10,
reliability=ReliabilityPolicy.RELIABLE,
durability=DurabilityPolicy.VOLATILE
)
# Use it when creating a publisher
publisher = self.create_publisher(String, 'topic', qos_profile)
Common QoS policies include:
- Reliability: Best effort vs. reliable delivery
- Durability: Volatile vs. transient local (whether messages persist for late-joining subscribers)
- History: Keep all vs. keep last N messages
- Deadline: Maximum time between messages
- Lifespan: How long messages are kept in the system
Creating a Subscriber Node
Here's an example of a subscriber node that receives messages:
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MinimalSubscriber(Node):
def __init__(self):
super().__init__('minimal_subscriber')
self.subscription = self.create_subscription(
String,
'topic',
self.listener_callback,
10)
self.subscription # prevent unused variable warning
def listener_callback(self, msg):
self.get_logger().info('I heard: "%s"' % msg.data)
def main(args=None):
rclpy.init(args=args)
minimal_subscriber = MinimalSubscriber()
rclpy.spin(minimal_subscriber)
minimal_subscriber.destroy_node()
rclpy.shutdown()
Topic Tools and Monitoring
ROS 2 provides several command-line tools for monitoring and debugging topic communication:
List Topics
ros2 topic list
Echo Topic Messages
ros2 topic echo /topic std_msgs/msg/String
Publish to a Topic
ros2 topic pub /topic std_msgs/msg/String "data: 'Hello'"
Get Topic Information
ros2 topic info /topic
Best Practices for Topic Design
Naming Conventions
- Use forward slashes to separate namespaces:
/arm/joint_states - Use descriptive names that clearly indicate the content
- Group related topics under common namespaces
Message Design
- Keep messages small and efficient
- Use appropriate data types for your application
- Consider bandwidth limitations in distributed systems
- Version your message types appropriately
Performance Considerations
- Use appropriate QoS settings for your use case
- Limit the frequency of high-bandwidth topics
- Use latching for static information that late-joining nodes need
- Consider using services for request-response communication when appropriate
Example: Sensor Data Pipeline
Here's a practical example of using nodes and topics for a sensor data pipeline:
import rclpy
from rclpy.node import Node
from sensor_msgs.msg import LaserScan
from geometry_msgs.msg import PointStamped
class SensorProcessor(Node):
def __init__(self):
super().__init__('sensor_processor')
# Subscribe to raw laser scan data
self.scan_subscription = self.create_subscription(
LaserScan,
'/laser_scan',
self.scan_callback,
10)
# Publish processed obstacle data
self.obstacle_publisher = self.create_publisher(
PointStamped,
'/obstacles',
10)
def scan_callback(self, msg):
# Process the laser scan to detect obstacles
# This is a simplified example
for i, range_val in enumerate(msg.ranges):
if range_val < 1.0 and range_val > msg.range_min:
# Create obstacle message
obstacle_msg = PointStamped()
obstacle_msg.header = msg.header
# Convert polar to Cartesian coordinates
angle = msg.angle_min + i * msg.angle_increment
obstacle_msg.point.x = range_val * math.cos(angle)
obstacle_msg.point.y = range_val * math.sin(angle)
obstacle_msg.point.z = 0.0
# Publish obstacle
self.obstacle_publisher.publish(obstacle_msg)
Exercises
-
Basic Publisher-Subscriber: Create a publisher node that publishes the current time and a subscriber node that prints the received time messages.
-
Sensor Pipeline: Implement a node that subscribes to a simulated IMU topic and publishes filtered orientation data.
-
Topic Monitoring: Use
ros2 topiccommands to monitor the communication between your nodes and analyze the message frequency and size.