4、第一个ROS2节点:Python发布者与订阅者
终于到了动手写代码的时候了。说实话,每次带新人入门ROS2,我最怕的就是一上来就扔出一堆概念——节点、话题、发布订阅、DDS……听着就头大。所以这一章,咱们直接写两个节点,一个说话,一个听话。跑通了,你自然就懂了。
4.1 理解发布者与订阅者模型
ROS2的通信模型,说白了就是“广播”模式。发布者(Publisher)只管往话题(Topic)上扔数据,它不知道谁在听。订阅者(Subscriber)只管从话题上收数据,它不知道谁在发。这种解耦设计,在机器人系统里特别实用——你想加个传感器节点,完全不用改其他节点的代码。
我刚开始做多传感器融合时,就是靠这个模型把激光雷达、IMU、里程计的数据全串起来的。每个传感器一个节点,各自发布数据,融合节点只管订阅就行。
核心概念速览:
- 节点(Node):ROS2中的最小执行单元,一个可执行程序
- 话题(Topic):节点间通信的“总线”,有名称和数据类型
- 发布者(Publisher):向话题发送消息的节点
- 订阅者(Subscriber):从话题接收消息的节点
下面这张图,把整个流程串起来了:
4.2 编写Python发布者节点
先创建一个工作空间。我个人习惯把代码都放在 ~/ros2_ws/src 下,这样好管理。
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python py_pubsub
这个命令会生成一个Python包的基本结构。进去看看:
cd py_pubsub
tree
你会看到 py_pubsub 目录、setup.py、package.xml 这些文件。现在,在 py_pubsub/py_pubsub/ 下新建一个 talker.py:
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class TalkerNode(Node):
def __init__(self):
super().__init__('talker')
self.publisher_ = self.create_publisher(String, 'chatter', 10)
self.timer = self.create_timer(1.0, self.timer_callback)
self.count = 0
def timer_callback(self):
msg = String()
msg.data = f'Hello ROS2: 第{self.count}次'
self.publisher_.publish(msg)
self.get_logger().info(f'发布: "{msg.data}"')
self.count += 1
def main(args=None):
rclpy.init(args=args)
node = TalkerNode()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
这段代码干了三件事:
- 创建一个名为
talker的节点 - 在
chatter话题上创建一个发布者,消息类型是String - 每1秒触发一次回调,发布一条消息
小提示: create_publisher 的第三个参数是队列大小(10)。这个值设太小容易丢消息,设太大占内存。我一般设10-20,够用。
4.3 编写Python订阅者节点
同样在 py_pubsub/py_pubsub/ 下新建 listener.py:
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class ListenerNode(Node):
def __init__(self):
super().__init__('listener')
self.subscription = self.create_subscription(
String, 'chatter', self.listener_callback, 10)
self.subscription # 防止被垃圾回收
def listener_callback(self, msg):
self.get_logger().info(f'收到: "{msg.data}"')
def main(args=None):
rclpy.init(args=args)
node = ListenerNode()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
订阅者的代码更简单——它只等着收消息,收到就打印出来。注意那个 self.subscription 赋值,我刚开始写的时候忘了这行,结果节点一启动就崩了。后来查文档才知道,Python的垃圾回收会把没人引用的订阅对象给收掉。
踩坑提醒: 订阅者节点中,self.subscription = self.create_subscription(...) 这行必须保留。我曾经因为删了它,调试了整整一下午才发现是引用丢失的问题。
4.4 配置入口点并编译
代码写好了,得告诉ROS2怎么启动它们。编辑 setup.py,找到 entry_points 部分:
entry_points={
'console_scripts': [
'talker = py_pubsub.talker:main',
'listener = py_pubsub.listener:main',
],
},
然后编译:
cd ~/ros2_ws
colcon build --packages-select py_pubsub
source install/setup.bash
编译成功后,开两个终端,都 source install/setup.bash。一个跑发布者,一个跑订阅者:
# 终端1
ros2 run py_pubsub talker
# 终端2
ros2 run py_pubsub listener
你会看到订阅者终端里,每秒打印一条消息。嗯,跑通了。
4.5 C++节点与Python节点的差异
很多初学者会问:到底用Python还是C++?我的建议是——原型用Python,产品用C++。下面这张表,是我多年项目经验的总结:
| 对比维度 | Python节点 | C++节点 |
|---|---|---|
| 开发速度 | 快,不用编译,改完就能跑 | 慢,每次改代码都要编译 |
| 运行性能 | 中等,适合控制频率≤100Hz | 高,适合高频控制、大计算量 |
| 内存管理 | 自动GC,但要注意引用丢失 | 手动管理,但更可控 |
| 包构建 | ament_python,无需CMake |
ament_cmake,需要CMakeLists.txt |
| 适用场景 | 原型验证、工具脚本、教学 | 实时控制、SLAM、导航 |
我记得有一次做激光雷达的实时SLAM,用Python写的节点,CPU直接飙到80%。换成C++重写后,降到15%。但反过来,做数据可视化工具时,Python的快速迭代优势就体现出来了。
4.6 使用colcon编译与运行
colcon 是ROS2的官方构建工具。常用的命令就这几个:
# 编译整个工作空间
colcon build
# 只编译某个包(省时间)
colcon build --packages-select py_pubsub
# 编译时忽略测试(加快速度)
colcon build --packages-select py_pubsub --cmake-args -DBUILD_TESTING=OFF
# 编译并符号链接(Python包推荐)
colcon build --packages-select py_pubsub --symlink-install
我的习惯: Python包一定加 --symlink-install。这样改了Python代码不用重新编译,直接重新运行节点就行。省了多少时间?你自己算算。
编译完别忘了 source install/setup.bash。我见过太多人编译完直接跑,结果报错说找不到包——其实就是忘了source。
运行节点用 ros2 run 命令:
ros2 run <包名> <节点名>
比如我们刚才的:
ros2 run py_pubsub talker
ros2 run py_pubsub listener
如果想看话题在发什么数据,可以开第三个终端:
ros2 topic echo /chatter
这个命令会实时打印话题上的所有消息,调试时特别好用。
4.7 本章小结
这一章,我们从零开始写了一个发布者节点和一个订阅者节点。你学会了:
- ROS2的发布-订阅通信模型
- 用Python编写Publisher和Subscriber
- 配置
setup.py的入口点 - 使用
colcon编译和ros2 run运行 - Python节点与C++节点的差异
说实话,这两个节点虽然简单,但它们是ROS2机器人开发的基石。后面所有的传感器驱动、SLAM算法、导航控制,本质上都是在这个模型上叠加更复杂的逻辑。把这个跑通了,后面的路就好走了。