第3章:ROS2基础编程:C++与Python节点编写、话题发布与订阅、自定义消息接口
好,咱们进入正题。这一章是ROS2编程的基石,说白了就是学会怎么让机器人“说话”和“听话”。你想想看,一个机器人要动起来,传感器得告诉电机“我测到障碍物了”,电机得告诉控制器“我转了几圈”。这些信息怎么传递?就是靠节点、话题和消息。
我个人习惯,学ROS2编程,先别急着跑,先把站姿练好。这一章我会带你手写C++和Python的节点,搞懂发布订阅模式,最后再教你定义自己的消息类型。嗯,这些都是我当年踩过坑的地方,咱们一步步来。
3.1 节点:机器人的“独立小脑”
节点是ROS2里最小的执行单元。你可以把它想象成一个独立的“小脑”,每个小脑只负责一件事:比如一个节点专门读激光雷达,另一个节点专门控制电机。它们之间通过话题通信,互不干扰。
为什么要这么设计?我刚开始做项目时,把所有代码写在一个文件里,结果改一个传感器驱动,整个系统都要重新编译。后来用了ROS2的节点化设计,每个模块独立开发、独立调试,爽多了。
3.1.1 用Python写一个节点
Python写节点,上手快,适合快速验证想法。来看一个最简单的例子:
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
class MyFirstNode(Node):
def __init__(self):
super().__init__('my_first_node')
self.get_logger().info('节点已启动!')
def main(args=None):
rclpy.init(args=args)
node = MyFirstNode()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
这段代码干了三件事:初始化ROS2、创建节点、让节点保持运行。注意那个rclpy.spin(),它就像个死循环,让节点一直活着,随时准备处理消息。
rclpy.init(),结果节点一直报错,找了半天才发现。
3.1.2 用C++写一个节点
C++版本性能更好,适合对实时性要求高的控制任务。同样的功能,C++写起来稍微啰嗦一点:
#include "rclcpp/rclcpp.hpp"
class MyFirstNode : public rclcpp::Node
{
public:
MyFirstNode() : Node("my_first_node")
{
RCLCPP_INFO(this->get_logger(), "节点已启动!");
}
};
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<MyFirstNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
你看,逻辑和Python版本一模一样。区别在于C++需要手动管理内存(用智能指针),而且编译时要链接rclcpp库。我个人建议,原型验证用Python,正式部署用C++。
3.2 话题:机器人的“消息高速公路”
话题是节点之间传递数据的通道。一个节点可以发布消息到话题,另一个节点可以订阅这个话题来接收消息。这种模式叫“发布-订阅”,说白了就是“我说你听,互不干扰”。
为什么会这样?因为机器人系统里,传感器数据往往是单向流动的。激光雷达只管发数据,不需要知道谁在用。电机控制器只管收指令,不需要知道指令从哪来。这种解耦设计,让系统更容易扩展。
3.2.1 发布者(Publisher)
发布者就是“说话”的节点。下面这个例子,每秒发布一次“Hello, ROS2!”字符串:
# Python版本
class TalkerNode(Node):
def __init__(self):
super().__init__('talker')
self.publisher_ = self.create_publisher(String, 'chatter', 10)
timer_period = 1.0 # 1秒
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}')
注意那个队列大小参数10。它表示如果订阅者处理不过来,最多缓存10条消息。我曾经在项目中把队列设成1,结果丢消息丢到怀疑人生。嗯,这里要注意,队列大小要根据你的数据频率和处理速度来调。
3.2.2 订阅者(Subscriber)
订阅者就是“听话”的节点。它收到消息后,会触发回调函数:
# Python版本
class ListenerNode(Node):
def __init__(self):
super().__init__('listener')
self.subscription = self.create_subscription(
String, 'chatter', self.listener_callback, 10)
def listener_callback(self, msg):
self.get_logger().info(f'收到了: {msg.data}')
回调函数是异步执行的。也就是说,发布者发完消息就继续干自己的事,订阅者收到消息后再处理。这种机制保证了系统不会因为某个节点卡住而整体瘫痪。
3.3 自定义消息接口
ROS2内置的消息类型(比如String、Int32)只能传简单数据。实际项目中,你经常需要传复杂结构,比如“位置+速度+时间戳”。这时候就需要自定义消息接口。
我当年做AGV小车时,需要传递“目标点坐标+允许误差+优先级”三个字段。用内置消息得拆成三个话题,麻烦得要死。后来学会了自定义消息,一个接口搞定所有。
3.3.1 定义.msg文件
在功能包下创建msg文件夹,新建TargetPoint.msg:
float32 x
float32 y
float32 tolerance
int32 priority
就这么简单。每个字段一行,格式是“类型 名称”。支持的类型包括int、float、string、数组,甚至嵌套其他消息。
3.3.2 修改CMakeLists.txt和package.xml
要让ROS2识别你的自定义消息,需要修改构建配置。CMakeLists.txt里添加:
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/TargetPoint.msg"
)
ament_export_dependencies(rosidl_default_runtime)
package.xml里添加:
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
member_of_group,结果编译通过但运行时找不到消息类型。这个坑我踩了整整一个下午,你千万别重蹈覆辙。
3.3.3 在代码中使用自定义消息
编译成功后,你就可以像使用内置消息一样使用自定义消息了:
# Python中使用自定义消息
from your_package.msg import TargetPoint
msg = TargetPoint()
msg.x = 1.5
msg.y = 2.0
msg.tolerance = 0.1
msg.priority = 1
C++中使用:
#include "your_package/msg/target_point.hpp"
auto msg = your_package::msg::TargetPoint();
msg.x = 1.5;
msg.y = 2.0;
msg.tolerance = 0.1;
msg.priority = 1;
3.4 本章知识体系总览
下面这张图,帮你理清节点、话题、消息之间的关系:
这张图展示了ROS2通信的核心模式。发布者节点把消息塞进话题,订阅者节点从话题里取消息。两者之间没有直接依赖,你可以随时增加新的发布者或订阅者,不影响现有系统。
3.5 实战建议
学完这一章,我建议你立刻动手做个小实验:写一个发布者每秒发布当前时间,再写一个订阅者打印收到的消息。先跑通Python版本,再换成C++版本。你会发现,核心逻辑完全一样,只是语法不同。
自定义消息这块,别怕麻烦。我刚开始也觉得定义.msg文件很啰嗦,但用过一次就真香了。它让代码更清晰,数据更规范,调试也更方便。
好了,这一章的内容就到这里。记住:节点是骨架,话题是血管,消息是血液。把这三样搞明白,ROS2编程你就入门了。