3、ROS2基础编程:C++与Python节点编写、发布者与订阅者实现、自定义消息接口、launch文件编写、参数服务器使用

好,咱们直接进入正题。这一章是ROS2的“动手”章节,说白了就是让你真正写代码跑起来。很多初学者喜欢先啃理论,我个人的习惯是——先让轮子转起来,再回头理解为什么这么转。所以这一章,我会带着你从零写一个节点,然后一步步把它变成能用的系统。

3.1 节点编写:C++与Python的“Hello World”

节点是ROS2的最小执行单元。你可以把它理解成一个独立的小程序,专门干一件事。比如一个节点负责读激光雷达,另一个节点负责规划路径。

先看C++版本。创建一个包,然后在src目录下写一个cpp文件:

#include "rclcpp/rclcpp.hpp"

int main(int argc, char **argv) {
    rclcpp::init(argc, argv);
    auto node = std::make_shared<rclcpp::Node>("my_first_cpp_node");
    RCLCPP_INFO(node->get_logger(), "C++节点已启动!");
    rclcpp::spin(node);
    rclcpp::shutdown();
    return 0;
}

这段代码干了三件事:初始化ROS2、创建节点、让节点保持运行。嗯,这里要注意,rclcpp::spin是个阻塞函数,它会一直等在那里,直到你按Ctrl+C。

再看Python版本,其实更简洁:

import rclpy
from rclpy.node import Node

def main(args=None):
    rclpy.init(args=args)
    node = Node("my_first_py_node")
    node.get_logger().info("Python节点已启动!")
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

我个人觉得Python版本更适合快速原型验证。我在项目中经常先用Python把逻辑跑通,再用C++重写一遍做性能优化。你想想看,调试阶段Python的print大法多方便啊。

3.2 发布者与订阅者:让节点“说话”

节点之间怎么通信?靠的就是发布者(Publisher)和订阅者(Subscriber)。一个节点发布消息,另一个节点接收消息。这就像电台和收音机的关系。

写一个发布者,每隔1秒发一个“Hello”字符串:

// C++ 发布者示例
auto publisher = node->create_publisher<std_msgs::msg::String>("topic_hello", 10);
rclcpp::WallRate rate(1.0);
while (rclcpp::ok()) {
    auto msg = std_msgs::msg::String();
    msg.data = "Hello, ROS2!";
    publisher->publish(msg);
    rate.sleep();
}

对应的订阅者:

// C++ 订阅者示例
auto subscription = node->create_subscription<std_msgs::msg::String>(
    "topic_hello", 10, [](const std_msgs::msg::String::SharedPtr msg) {
        RCLCPP_INFO(rclcpp::get_logger("subscriber"), "收到: %s", msg->data.c_str());
    });

这里有个坑,我曾经遇到过——队列长度设成10是什么意思?说白了就是缓冲区大小。如果订阅者处理速度跟不上发布者,消息会积压。队列满了之后,最老的消息会被丢弃。所以要根据你的实际场景调整这个值,不是越大越好。

3.3 自定义消息接口:别再用“万能”字符串了

用标准消息类型(比如String、Int32)做练习没问题,但真实项目中,你需要定义自己的消息结构。比如一个“车辆状态”消息,包含速度、转向角、GPS坐标。

在包的msg目录下创建VehicleStatus.msg:

float32 speed
float32 steering_angle
float64 latitude
float64 longitude
string vehicle_id

然后在CMakeLists.txt或setup.py中注册这个消息接口。编译之后,你就可以像使用标准消息一样使用它了:

# Python中使用自定义消息
from my_interfaces.msg import VehicleStatus

msg = VehicleStatus()
msg.speed = 30.5
msg.steering_angle = 0.2

我个人建议,消息字段尽量用基础类型,别嵌套太深。嵌套太深的消息,序列化和反序列化会消耗大量CPU,在嵌入式平台上尤其明显。

3.4 Launch文件编写:一键启动所有节点

手动启动每个节点太累了。Launch文件就是用来批量启动节点的。ROS2的Launch文件用Python写,比ROS1的XML灵活得多。

一个简单的launch文件:

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package='my_package',
            executable='my_cpp_node',
            name='cpp_node',
            output='screen'
        ),
        Node(
            package='my_package',
            executable='my_py_node',
            name='py_node',
            output='screen'
        ),
    ])

你还可以在launch文件中设置参数、条件判断、包含其他launch文件。我在做多传感器融合项目时,一个launch文件里启动了十几个节点,包括相机驱动、激光雷达驱动、IMU驱动、融合算法节点。没有launch文件,光启动这些节点就能让人崩溃。

3.5 参数服务器使用:让节点“可配置”

参数服务器说白了就是一个全局的键值对存储。你可以在启动时设置参数,也可以在运行时动态修改。

声明参数并获取:

// C++ 参数示例
node->declare_parameter("max_speed", 10.0);
double max_speed = node->get_parameter("max_speed").as_double();

在launch文件中设置参数:

Node(
    package='my_package',
    executable='my_node',
    parameters=[{'max_speed': 20.0}]
)

这里有个经验之谈——参数不要滥用。我曾经见过一个项目,把算法内部的所有常数都放到参数服务器里,结果参数文件有几百行,维护起来非常痛苦。我的建议是:只有那些需要外部配置的、可能经常变化的值才用参数。算法内部的固定常数,直接写在代码里就好。

核心要点总结:

  • 节点是ROS2的最小执行单元,一个节点干一件事
  • 发布者/订阅者实现节点间通信,队列长度要按需设置
  • 自定义消息接口用.msg文件定义,编译后自动生成代码
  • Launch文件用Python写,支持条件判断和参数传递
  • 参数服务器用于外部配置,不要把所有常数都放进去

小技巧:调试时可以用ros2 topic echo /topic_name查看话题内容,用ros2 param list查看所有参数。这两个命令我几乎每天都要用。

注意:自定义消息接口修改后,一定要重新编译整个包,否则会出现消息类型不匹配的错误。我曾经因为这个bug排查了整整一下午。

ROS2基础编程知识体系 ROS2节点 C++节点编写 Python节点编写 发布者与订阅者 自定义消息接口 Launch文件编写 参数服务器 五个核心模块构成ROS2基础编程的完整知识闭环 创建 创建 通信 定义 启动 配置

这张图把本章的知识点串起来了。你看,所有东西都围绕着“节点”这个核心。节点怎么创建、怎么通信、怎么定义数据、怎么批量启动、怎么配置参数——把这五个点吃透,ROS2基础编程你就入门了。

最后说一句,代码一定要亲手敲一遍。看十遍不如写一遍,写一遍不如调试一遍。遇到问题别怕,ROS2的错误信息其实挺友好的,仔细看日志,大部分问题都能自己解决。

可可精品资料库