4、话题通信实战:发布者与订阅者的C++实现、自定义消息类型

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

前面几章我们把ROS的概念、文件系统、命令行工具都过了一遍。说实话,那些都是开胃菜。真正让ROS跑起来、让机器人动起来的核心,就是话题通信

我个人习惯把话题通信比作「广播电台」。发布者就是电台主持人,一直在那说话;订阅者就是听众,打开收音机就能听到。主持人不知道谁在听,听众也不知道主持人长啥样。但信息就这么传过去了。

这一章,我们就亲手搭建一个这样的「电台」。

4.1 发布者(Publisher)的C++实现

先写发布者。说白了,就是往某个话题上发消息的节点。

我刚开始学的时候,总觉得写ROS节点很神秘。后来发现,套路极其固定。你只要记住几个步骤就行。

发布者核心步骤:

  1. 初始化ROS节点
  2. 创建节点句柄
  3. 通过句柄发布话题
  4. 循环发布消息

来,直接看代码。我习惯在功能包的src目录下新建一个文件,叫talker.cpp

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

int main(int argc, char **argv)
{
    // 1. 初始化节点,节点名就叫"talker"
    ros::init(argc, argv, "talker");

    // 2. 创建节点句柄
    ros::NodeHandle nh;

    // 3. 发布话题"chatter",消息类型是std_msgs::String
    //    队列大小设为1000,意思是如果订阅者处理不过来,最多缓存1000条
    ros::Publisher chatter_pub = nh.advertise<std_msgs::String>("chatter", 1000);

    // 4. 设置循环频率,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();

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

        ROS_INFO("我发布了: %s", msg.data.c_str());

        // 休眠,控制频率
        loop_rate.sleep();
        ++count;
    }

    return 0;
}

嗯,这里要注意一点。ros::ok()这个函数很关键。它会检查节点是否被要求关闭。比如你在终端按了Ctrl+C,它就会返回false,循环就退出了。我曾经见过有人写死循环,结果节点关不掉,只能kill进程。

4.2 订阅者(Subscriber)的C++实现

有发的,就得有收的。订阅者代码同样套路固定。

小技巧:我建议你先把发布者跑起来,再启动订阅者。这样订阅者一启动就能收到数据。反过来,如果订阅者先启动,它就会在那干等,直到发布者上线。

新建一个listener.cpp文件:

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

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

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

    // 订阅话题"chatter",队列大小1000
    // 一旦有消息,就调用chatterCallback
    ros::Subscriber sub = nh.subscribe("chatter", 1000, chatterCallback);

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

    return 0;
}

看到ros::spin()了吗?这个函数会一直阻塞,直到节点关闭。它背后的原理是:不断检查有没有新消息到达,有的话就触发回调函数。

为什么不用while循环?因为ros::spin()内部做了优化,不会浪费CPU。你想想看,如果自己写while(true)加sleep,效率就差远了。

4.3 自定义消息类型

标准消息类型(比如std_msgs::String)只能传简单的数据。但实际项目中,我们经常需要传复杂结构。比如机器人的位置、速度、姿态——这些信息打包在一起才方便。

这时候就需要自定义消息了。

避坑指南:我曾经在项目里直接往话题里发JSON字符串,结果解析起来麻烦得要死。后来老老实实自定义消息,代码瞬间清爽了。别走弯路。

步骤很简单,就三步:

  1. 在功能包下创建msg目录
  2. 编写.msg文件
  3. 修改CMakeLists.txtpackage.xml

举个例子,我们定义一个机器人位置的消息:

// 文件: msg/RobotPos.msg
float32 x
float32 y
float32 theta
string name

然后修改CMakeLists.txt,找到这几行,取消注释并修改:

find_package(catkin REQUIRED COMPONENTS
  roscpp
  std_msgs
  message_generation   # 加上这个
)

add_message_files(
  FILES
  RobotPos.msg
)

generate_messages(
  DEPENDENCIES
  std_msgs
)

catkin_package(
  CATKIN_DEPENDS message_runtime   # 加上这个
)

再修改package.xml

<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>

编译一下:

cd ~/catkin_ws
catkin_make

编译成功后,你的自定义消息类型就生成了。在代码里可以这样用:

#include "你的包名/RobotPos.h"

// 发布者
ros::Publisher pub = nh.advertise<你的包名::RobotPos>("robot_pos", 10);
你的包名::RobotPos pos_msg;
pos_msg.x = 1.5;
pos_msg.y = 2.3;
pos_msg.theta = 0.78;
pos_msg.name = "turtlebot";
pub.publish(pos_msg);

// 订阅者
void posCallback(const 你的包名::RobotPos::ConstPtr& msg)
{
    ROS_INFO("位置: x=%f, y=%f", msg->x, msg->y);
}
ros::Subscriber sub = nh.subscribe("robot_pos", 10, posCallback);

4.4 编译与运行

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

首先,确保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

启动roscore,再分别运行两个节点:

# 终端1
roscore

# 终端2
rosrun 你的包名 talker

# 终端3
rosrun 你的包名 listener

你会看到发布者每秒发10条消息,订阅者实时收到并打印出来。

核心总结:

  • 发布者用advertise,订阅者用subscribe
  • 订阅者通过回调函数处理消息
  • 自定义消息需要三步:写msg文件、改CMakeLists、改package.xml
  • 编译后自动生成头文件,直接include就能用

这一章的内容,说白了就是ROS通信的基石。你把这个搞懂了,后面学服务通信、动作通信都会轻松很多。下一章我们聊聊话题通信的进阶用法——包括消息过滤器、时间同步这些实战中经常遇到的东西。

嗯,今天就到这儿。动手试试吧。