3. ROS2实战:编写第一个发布订阅节点、编译与运行、使用rqt工具可视化

好,咱们开始动手了。

前面聊了那么多中间件的理论,说实话,不写几行代码总觉得心里不踏实。这一章,我带你亲手搭建一个ROS2的发布订阅节点。你想想看,这其实就是自动驾驶系统里最基础的通信单元——一个节点发消息,另一个节点收消息。搞懂了它,后面那些复杂的传感器融合、规划控制,本质上都是这个模式的延伸。

3.1 准备工作:工作空间与包结构

我个人习惯,先把工作空间搭好。ROS2里叫workspace,说白了就是一个放代码的文件夹。

# 创建工作空间
mkdir -p ~/autoware_ws/src
cd ~/autoware_ws/src

# 创建功能包
ros2 pkg create --build-type ament_cmake --node-name talker_node beginner_tutorials

这里我用了ament_cmake,因为C++在自动驾驶里更常见。如果你偏爱Python,也可以用ament_python。嗯,这里要注意:包名不要用大写字母,ROS2的规范是小写加下划线。

创建完成后,你会看到这样的目录结构:

beginner_tutorials/
├── CMakeLists.txt
├── package.xml
├── src/
│   └── talker_node.cpp
└── include/
    └── beginner_tutorials/
小技巧: 我习惯在src/目录下再建一个nodes/文件夹,把不同功能的节点分开。项目大了你就知道,这样找代码快很多。

3.2 编写发布者节点(Talker)

打开src/talker_node.cpp,我们来写第一个发布者。说白了,就是每隔一秒发一条"Hello, 自动驾驶!"的消息。

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

class TalkerNode : public rclcpp::Node
{
public:
    TalkerNode() : Node("talker_node")
    {
        publisher_ = this->create_publisher<std_msgs::msg::String>("topic_chatter", 10);
        timer_ = this->create_wall_timer(
            std::chrono::seconds(1),
            std::bind(&TalkerNode::timer_callback, this));
        RCLCPP_INFO(this->get_logger(), "发布者节点已启动");
    }

private:
    void timer_callback()
    {
        auto message = std_msgs::msg::String();
        message.data = "Hello, 自动驾驶!当前时间: " + std::to_string(count_++);
        RCLCPP_INFO(this->get_logger(), "发布: '%s'", message.data.c_str());
        publisher_->publish(message);
    }

    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
    rclcpp::TimerBase::SharedPtr timer_;
    int count_ = 0;
};

int main(int argc, char *argv[])
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<TalkerNode>());
    rclcpp::shutdown();
    return 0;
}

这段代码里,我用了create_wall_timer来定时触发。为什么不用sleep?因为sleep会阻塞整个线程,而定时器是异步的,不影响节点处理其他回调。我在项目中遇到过,有人用sleep写节点,结果一跑起来,订阅端收不到消息,排查了半天才发现是定时器用错了。

3.3 编写订阅者节点(Listener)

有发就得有收。我们再写一个订阅者节点,放在同一个包里。

在src/目录下新建listener_node.cpp:

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

class ListenerNode : public rclcpp::Node
{
public:
    ListenerNode() : Node("listener_node")
    {
        subscription_ = this->create_subscription<std_msgs::msg::String>(
            "topic_chatter", 10,
            std::bind(&ListenerNode::topic_callback, this, std::placeholders::_1));
        RCLCPP_INFO(this->get_logger(), "订阅者节点已启动,等待消息...");
    }

private:
    void topic_callback(const std_msgs::msg::String::SharedPtr msg)
    {
        RCLCPP_INFO(this->get_logger(), "收到: '%s'", msg->data.c_str());
    }

    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};

int main(int argc, char *argv[])
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<ListenerNode>());
    rclcpp::shutdown();
    return 0;
}

你看,订阅者的结构和发布者很像。区别在于,它注册了一个回调函数topic_callback,一旦有消息到达,这个函数就会被调用。说白了,就是"你发我就收,你不发我等着"。

3.4 修改CMakeLists.txt

代码写完了,得告诉编译器怎么编译。打开CMakeLists.txt,在文件末尾添加:

add_executable(talker_node src/talker_node.cpp)
ament_target_dependencies(talker_node rclcpp std_msgs)

add_executable(listener_node src/listener_node.cpp)
ament_target_dependencies(listener_node rclcpp std_msgs)

install(TARGETS
    talker_node
    listener_node
    DESTINATION lib/${PROJECT_NAME}
)
注意: 我曾经犯过一个低级错误——忘记在package.xml里添加rclcpp和std_msgs的依赖。结果编译报错,提示找不到头文件。所以记得检查package.xml,确保有这两行:
<depend>rclcpp</depend>
<depend>std_msgs</depend>

3.5 编译与运行

回到工作空间根目录,开始编译:

cd ~/autoware_ws
colcon build --packages-select beginner_tutorials

编译成功后,记得source一下环境:

source install/setup.bash

然后,开两个终端,分别运行发布者和订阅者:

终端1(发布者):

ros2 run beginner_tutorials talker_node

终端2(订阅者):

ros2 run beginner_tutorials listener_node

你会看到,发布者每秒打印一条发送的消息,订阅者同步打印收到的消息。嗯,这就是最基础的发布订阅通信。

3.6 使用rqt工具可视化

光看终端日志不过瘾,咱们用rqt来可视化。rqt是ROS2的图形化工具集,说白了就是给你一个GUI,让你看到节点、话题、服务这些内部结构。

先启动rqt:

rqt

在rqt的菜单栏里,选择 Plugins > Topics > Topic Monitor。你会看到topic_chatter这个话题,点击它,就能看到消息的实时内容。

我个人最常用的是rqt_graph,它能画出节点之间的通信关系图。在插件菜单里找到它,你会看到:

/talker_node 发布者 /topic_chatter std_msgs/String /listener_node 订阅者 rqt_graph 可视化效果 绿色箭头:发布方向 | 蓝色箭头:订阅方向 ROS2 发布订阅通信模型

这张图很直观吧?/talker_node把消息推到/topic_chatter上,/listener_node从上面取。箭头方向就是数据流的方向。

核心要点: 在自动驾驶系统中,这种模式无处不在。比如激光雷达节点发布点云数据,感知节点订阅它;规划节点发布轨迹,控制节点订阅它。搞懂了这个基础模型,整个系统的通信架构你就理解了一半。

3.7 避坑指南

最后,分享几个我踩过的坑:

  • 话题名称拼写错误:发布者和订阅者的话题名必须完全一致,大小写敏感。我曾经因为把chatter拼成chater,排查了半小时。
  • 忘记source环境:每次新开终端,都要source install/setup.bash。我建议把它加到~/.bashrc里,省得每次都手动敲。
  • QoS设置不匹配:ROS2的QoS(服务质量)如果发布者和订阅者设置不一致,可能导致收不到消息。初学者先用默认的10,后面再深入调优。
  • 节点名冲突:同一个网络中,节点名不能重复。如果你同时跑两个talker_node,ROS2会报错。可以用ros2 run ... --ros-args -r __node:=new_name来重命名。

好了,这一章的内容就到这里。代码不多,但每一步都是自动驾驶中间件开发的基石。你动手跑一遍,比看十遍文档都管用。

可可精品资料库