第1章:ROS2核心概念与实战
各位同学好,我是你们这门课的主讲。今天咱们聊聊ROS2里最基础、也最重要的四个概念:节点、话题、服务、动作。
说实话,我见过太多人一上来就啃文档,结果越看越晕。其实没那么复杂。你想想看,自动驾驶系统本质上就是一堆程序在互相通信。那怎么通信?就是靠这四样东西。
1.1 节点(Node)——系统的“人”
节点是什么?说白了,就是一个独立的可执行程序。每个节点负责一件事。比如在Autoware里,有感知节点、规划节点、控制节点。
我个人习惯把节点想象成团队里的成员。每个人有自己的职责,大家互相配合。你不可能让一个人既开车又看路况又做决策,对吧?
核心要点:节点是ROS2的最小执行单元。每个节点都是一个独立的进程。
写一个节点其实很简单。我给大家看个例子:
import rclpy
from rclpy.node import Node
class MyNode(Node):
def __init__(self):
super().__init__('my_first_node')
self.get_logger().info('节点已启动!')
def main(args=None):
rclpy.init(args=args)
node = MyNode()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
这段代码创建了一个叫my_first_node的节点。启动后它会打印一条日志。嗯,这里要注意:rclpy.spin()会让节点一直运行,直到你按Ctrl+C。
小技巧:我刚开始学的时候,总忘记调用rclpy.init()。结果节点一直启动不了。后来我养成了习惯,写节点第一步就是检查init有没有写。
1.2 话题(Topic)——系统的“广播”
话题是ROS2里最常用的通信方式。它是一种发布-订阅模式。一个节点发布消息到某个话题,其他节点订阅这个话题就能收到消息。
我曾经在项目里遇到过一个问题:两个节点用话题通信,但数据总是对不上。查了半天才发现,是话题名称拼写错了。你想想看,一个字母不对,整个系统就瘫痪了。
发布者的代码长这样:
class PublisherNode(Node):
def __init__(self):
super().__init__('publisher_node')
self.publisher_ = self.create_publisher(String, 'chatter', 10)
timer_period = 0.5 # 每秒发两次
self.timer = self.create_timer(timer_period, self.timer_callback)
def timer_callback(self):
msg = String()
msg.data = 'Hello, ROS2!'
self.publisher_.publish(msg)
self.get_logger().info(f'发布了: {msg.data}')
订阅者呢?
class SubscriberNode(Node):
def __init__(self):
super().__init__('subscriber_node')
self.subscription = self.create_subscription(
String, 'chatter', self.listener_callback, 10)
def listener_callback(self, msg):
self.get_logger().info(f'收到了: {msg.data}')
避坑指南:我曾经因为队列大小设置太小,导致高频数据丢失。建议根据你的数据频率来设置队列大小。比如摄像头数据,队列至少设到100以上。
1.3 服务(Service)——系统的“问答”
话题是单向的,发布者只管发,不管有没有人收。但服务不一样,它是请求-响应模式。客户端发一个请求,服务端处理完返回结果。
我举个例子。在Autoware里,你要让车辆启动,就会调用一个服务。你发请求说“启动”,系统处理完告诉你“已启动”或“启动失败”。
服务端代码:
from example_interfaces.srv import AddTwoInts
class ServiceNode(Node):
def __init__(self):
super().__init__('service_node')
self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_callback)
def add_callback(self, request, response):
response.sum = request.a + request.b
self.get_logger().info(f'收到请求: {request.a} + {request.b} = {response.sum}')
return response
客户端代码:
class ClientNode(Node):
def __init__(self):
super().__init__('client_node')
self.cli = self.create_client(AddTwoInts, 'add_two_ints')
while not self.cli.wait_for_service(timeout_sec=1.0):
self.get_logger().info('等待服务端启动...')
self.send_request(3, 5)
def send_request(self, a, b):
request = AddTwoInts.Request()
request.a = a
request.b = b
future = self.cli.call_async(request)
future.add_done_callback(self.response_callback)
def response_callback(self, future):
response = future.result()
self.get_logger().info(f'结果: {response.sum}')
关键区别:话题是“发完就不管了”,服务是“问了就得等答案”。选哪个?看你需不需要立即知道结果。
1.4 动作(Action)——系统的“长任务”
动作是ROS2里最复杂的通信方式。它适合执行时间较长的任务,比如导航到某个地点。动作有三个阶段:发送目标、反馈进度、返回结果。
我记得第一次用动作时,觉得它跟服务差不多。后来才发现,动作可以中途取消,还能实时反馈进度。服务做不到这些。
动作服务端示例:
from action_tutorials_interfaces.action import Fibonacci
class ActionServerNode(Node):
def __init__(self):
super().__init__('action_server_node')
self.action_server = ActionServer(
self, Fibonacci, 'fibonacci', self.execute_callback)
async def execute_callback(self, goal_handle):
self.get_logger().info('开始执行...')
feedback_msg = Fibonacci.Feedback()
feedback_msg.sequence = [0, 1]
for i in range(1, goal_handle.request.order):
feedback_msg.sequence.append(
feedback_msg.sequence[i] + feedback_msg.sequence[i-1])
goal_handle.publish_feedback(feedback_msg)
await asyncio.sleep(1)
goal_handle.succeed()
result = Fibonacci.Result()
result.sequence = feedback_msg.sequence
return result
实用建议:如果你的任务可能超过5秒,或者需要中途取消,用动作准没错。比如自动驾驶里的变道、泊车,都是动作的典型应用。
1.5 知识体系总览
说了这么多,我画张图帮你理清思路:
1.6 实战建议
学到这里,你可能觉得内容有点多。别急,我分享几个实战经验:
- 先跑通再优化:我见过太多人一开始就追求完美代码。其实先让节点跑起来,能通信了,再慢慢优化。
- 善用命令行工具:
ros2 topic list、ros2 service list这些命令,调试时能救命。 - 日志是你的朋友:多打日志,少用print。ROS2的日志有级别控制,方便排查问题。
重要提醒:千万别在生产环境里用rclpy.spin_once()加延时循环。我吃过这个亏,CPU占用率直接拉满。用rclpy.spin()或者定时器才是正道。
好了,这一章的内容就到这。记住这四个概念,后面的课程你会越来越顺。有什么问题,随时交流。