第4章:话题通信实战——编写发布者节点、编写订阅者节点、编译与运行、使用rostopic命令行工具

好,咱们今天来点真格的。

前面聊了那么多理论,什么Node、Master、Topic,说白了都是铺垫。这一章,我带你亲手写一个发布者节点和一个订阅者节点。你想想看,代码跑起来的那一刻,消息在节点间流动,那种感觉才叫真正入门了ROS。

我个人习惯,学新东西一定要先跑通一个最小例子。所以咱们就从最简单的“Hello World”开始——一个节点发字符串,另一个节点收字符串。

4.1 编写发布者节点

发布者节点,就是那个主动往外“喊话”的角色。它负责创建一个话题,然后按一定频率往话题上发消息。

咱们用C++写。先创建一个包,名字就叫topic_demo

cd ~/catkin_ws/src
catkin_create_pkg topic_demo roscpp std_msgs

然后在topic_demo/src目录下新建一个文件,叫talker.cpp

#include "ros/ros.h"
#include "std_msgs/String.h"
#include <sstream>

int main(int argc, char **argv)
{
    // 初始化ROS节点,名字叫"talker"
    ros::init(argc, argv, "talker");
    ros::NodeHandle n;

    // 创建一个发布者,话题叫"chatter",消息类型是std_msgs::String
    ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter", 1000);

    // 设置循环频率为10Hz
    ros::Rate loop_rate(10);

    int count = 0;
    while (ros::ok())
    {
        std_msgs::String msg;
        std::stringstream ss;
        ss << "hello world " << count;
        msg.data = ss.str();

        ROS_INFO("%s", msg.data.c_str());

        // 发布消息
        chatter_pub.publish(msg);

        ros::spinOnce();
        loop_rate.sleep();
        ++count;
    }

    return 0;
}

这段代码,我拆开给你讲。

ros::init是必须的,它告诉ROS系统这个节点的名字。注意节点名不能重复,否则会冲突。我在项目中遇到过好几次,两个节点不小心起了同一个名字,结果其中一个死活连不上——排查了半天才发现是名字撞了。

n.advertise这行,说白了就是“注册”一个发布者。第二个参数1000是消息队列大小。什么意思呢?如果订阅者处理得慢,消息会先缓存起来。1000就是最多缓存1000条。你想想看,如果设得太小,消息丢了;设得太大,内存扛不住。一般1000是个安全值。

循环里的ros::ok(),它会检查节点是否被要求关闭。比如你按了Ctrl+C,它就返回false,循环退出。

嗯,这里要注意:ros::spinOnce()不能省。虽然这个发布者节点没有订阅任何话题,但ROS内部有一些回调需要处理。不加的话,某些情况下节点会卡死。

4.2 编写订阅者节点

有喊话的,就得有听墙根的。订阅者节点就是那个“听”的角色。

在同一个包下,新建src/listener.cpp

#include "ros/ros.h"
#include "std_msgs/String.h"

// 回调函数,收到消息后自动调用
void chatterCallback(const std_msgs::String::ConstPtr& msg)
{
    ROS_INFO("I heard: [%s]", msg->data.c_str());
}

int main(int argc, char **argv)
{
    ros::init(argc, argv, "listener");
    ros::NodeHandle n;

    // 订阅"chatter"话题,队列大小1000
    ros::Subscriber sub = n.subscribe("chatter", 1000, chatterCallback);

    // 进入循环,等待回调
    ros::spin();

    return 0;
}

这段代码更简单。核心就是n.subscriberos::spin()

n.subscribe告诉ROS:我要听“chatter”这个话题,来了消息就调用chatterCallback这个函数。注意第三个参数是回调函数的名字,不加括号。

ros::spin()是个阻塞函数。它会一直运行,直到节点被关闭。它背后的逻辑是:不断检查有没有新消息,有的话就触发回调。我曾经见过新手把spin()spinOnce()搞混,结果回调函数只执行了一次就退出了——嗯,这个坑我踩过。

小提示:如果你想让订阅者节点做更多事情(比如同时处理其他逻辑),可以用ros::spinOnce()配合你自己的循环。但如果你只需要接收消息,ros::spin()是最省事的写法。

4.3 编译与运行

代码写完了,怎么让它跑起来?

首先,修改CMakeLists.txt。在文件末尾加上:

add_executable(talker src/talker.cpp)
target_link_libraries(talker ${catkin_LIBRARIES})

add_executable(listener src/listener.cpp)
target_link_libraries(listener ${catkin_LIBRARIES})

然后编译:

cd ~/catkin_ws
catkin_make

如果一切顺利,你会看到编译成功的提示。如果有报错,多半是拼写错误或者包依赖没加。我建议你养成好习惯:每次修改代码后都catkin_make一下,别攒一堆再编译,否则找bug找到崩溃。

运行之前,先启动roscore:

roscore

新开一个终端,运行发布者:

source ~/catkin_ws/devel/setup.bash
rosrun topic_demo talker

再开一个终端,运行订阅者:

source ~/catkin_ws/devel/setup.bash
rosrun topic_demo listener

你会看到订阅者的终端里,不断打印出“I heard: [hello world 0]”、“I heard: [hello world 1]”……

那一刻,说实话,挺有成就感的。我第一次跑通的时候,盯着那个终端看了好几分钟,觉得这东西真神奇。

4.4 使用rostopic命令行工具

代码跑起来了,但怎么确认它真的在工作?

ROS提供了一套命令行工具,专门用来调试话题通信。最常用的就是rostopic系列。

先看看当前有哪些话题:

rostopic list

你会看到输出里有一个/chatter,这就是咱们发布的话题。

想看看消息长什么样?直接监听:

rostopic echo /chatter

终端里会实时打印出每一条消息的内容。这个命令我调试时几乎必用——它能帮你确认发布者是不是真的在发数据。

想知道消息的发送频率?用hz命令:

rostopic hz /chatter

它会统计每秒收到多少条消息。如果频率不对,说明你的loop_rate设置有问题,或者系统负载太高。

还有info命令,可以查看话题的详细信息:

rostopic info /chatter

它会告诉你消息类型、发布者和订阅者的数量。这个命令在排查“为什么收不到消息”时特别有用。我曾经遇到一个情况:订阅者一直没反应,用rostopic info一看,发现发布者和订阅者的话题名字拼写差了一个字母——这种低级错误,工具一查就现原形。

核心总结:
  • 发布者用advertise注册,订阅者用subscribe注册
  • 回调函数是订阅者的灵魂,消息来了自动触发
  • ros::spin()ros::spinOnce()的区别要记牢
  • rostopic echo/hz/info是调试三件套,用好它们能省一半时间
避坑指南:我曾经在项目中遇到一个诡异问题——发布者明明在发消息,但订阅者就是收不到。折腾了半天,发现是roscore没启动。记住:没有Master,节点之间无法建立连接。另外,source setup.bash这一步也容易忘,忘了的话rosrun会找不到你的包。

好了,这一章的内容就到这里。代码不多,但每一步都是实打实的操作。我建议你亲手敲一遍,别复制粘贴。敲代码的过程,其实就是在建立肌肉记忆。

下一章,咱们会深入消息类型,聊聊怎么自定义消息。到时候你会发现,话题通信的威力远不止发个字符串这么简单。