4. ROS通信机制:Topic、Service、Action编程实战、自定义消息类型

ROS的通信机制,说白了就是机器人各个模块之间怎么“说话”。我刚开始接触ROS时,觉得这玩意儿不就是发发数据嘛,后来才发现,选错通信方式,系统跑起来会出大问题。今天咱们就把Topic、Service、Action这三个核心机制掰开揉碎,再聊聊自定义消息类型——这可是你搭建复杂系统的基本功。

4.1 三种通信机制的核心区别

先给个直观的对比。你想想看,一个机器人要完成“抓取杯子”这个任务:

  • Topic(话题):像广播电台。摄像头一直在发图像数据,谁想听谁订阅。适合连续、高频、单向的数据流。
  • Service(服务):像打电话。你问一句“杯子在哪?”,对方回答“在桌上”。适合请求-响应、一次性的任务。
  • Action(动作):像发快递。你下单后,快递员会告诉你“已取件”、“运输中”、“已送达”,还能中途取消。适合长时间、可反馈、可取消的任务。

我的经验:在SLAM建图时,激光雷达数据用Topic发布(每秒10帧),地图保存用Service(点一下保存),导航到目标点用Action(可以实时看进度)。选错了,系统要么延迟爆炸,要么逻辑混乱。

4.2 Topic编程实战

Topic是最常用的通信方式。我习惯把它比作“发布-订阅”模式。写一个简单的发布者和订阅者,你就懂了。

4.2.1 发布者(Publisher)

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

int main(int argc, char **argv)
{
    ros::init(argc, argv, "talker");
    ros::NodeHandle nh;
    
    // 创建一个发布者,话题名为"chatter",消息类型为std_msgs::String
    ros::Publisher pub = nh.advertise<std_msgs::String>("chatter", 10);
    
    ros::Rate loop_rate(10); // 10Hz
    int count = 0;
    
    while (ros::ok())
    {
        std_msgs::String msg;
        msg.data = "hello world " + std::to_string(count);
        
        ROS_INFO("发布消息: %s", msg.data.c_str());
        pub.publish(msg);
        
        ros::spinOnce();
        loop_rate.sleep();
        count++;
    }
    
    return 0;
}

4.2.2 订阅者(Subscriber)

// 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",回调函数处理数据
    ros::Subscriber sub = nh.subscribe("chatter", 10, chatterCallback);
    
    ros::spin(); // 进入循环,等待回调
    
    return 0;
}

避坑指南:我曾经在队列大小上吃过亏。队列大小设为1,如果处理速度跟不上发布速度,数据会丢。SLAM中图像数据建议设大一点(比如10),不然关键帧丢了,地图就飘了。

4.3 Service编程实战

Service适合“问一次,答一次”的场景。比如你让机器人保存当前地图,它保存完告诉你“好了”。

4.3.1 服务端(Server)

// server.cpp
#include "ros/ros.h"
#include "beginner_tutorials/AddTwoInts.h"

bool add(beginner_tutorials::AddTwoInts::Request  &req,
         beginner_tutorials::AddTwoInts::Response &res)
{
    res.sum = req.a + req.b;
    ROS_INFO("请求: a=%ld, b=%ld, 响应: sum=%ld", (long int)req.a, (long int)req.b, (long int)res.sum);
    return true;
}

int main(int argc, char **argv)
{
    ros::init(argc, argv, "add_two_ints_server");
    ros::NodeHandle nh;
    
    ros::ServiceServer service = nh.advertiseService("add_two_ints", add);
    ROS_INFO("服务已就绪");
    ros::spin();
    
    return 0;
}

4.3.2 客户端(Client)

// client.cpp
#include "ros/ros.h"
#include "beginner_tutorials/AddTwoInts.h"
#include <cstdlib>

int main(int argc, char **argv)
{
    ros::init(argc, argv, "add_two_ints_client");
    
    ros::NodeHandle nh;
    ros::ServiceClient client = nh.serviceClient<beginner_tutorials::AddTwoInts>("add_two_ints");
    
    beginner_tutorials::AddTwoInts srv;
    srv.request.a = atoll(argv[1]);
    srv.request.b = atoll(argv[2]);
    
    if (client.call(srv))
    {
        ROS_INFO("结果: %ld", (long int)srv.response.sum);
    }
    else
    {
        ROS_ERROR("服务调用失败");
        return 1;
    }
    
    return 0;
}

注意:Service是同步的。客户端调用后必须等服务器响应,期间会阻塞。如果你在控制循环里频繁调用Service,小心系统卡死。我见过有人用Service做实时控制,结果机器人一顿一顿的。

4.4 Action编程实战

Action是Service的升级版。它支持:

  • 目标(Goal):发起任务
  • 反馈(Feedback):实时汇报进度
  • 结果(Result):最终输出
  • 取消(Cancel):随时中止

举个例子,导航到目标点:

// 定义Action文件(Fibonacci.action)
# 目标
int32 order
---
# 结果
int32[] sequence
---
# 反馈
int32[] sequence

4.4.1 Action服务端

// fibonacci_server.cpp
#include <ros/ros.h>
#include <actionlib/server/simple_action_server.h>
#include "beginner_tutorials/FibonacciAction.h"

class FibonacciAction
{
protected:
    ros::NodeHandle nh_;
    actionlib::SimpleActionServer<beginner_tutorials::FibonacciAction> as_;
    std::string action_name_;
    beginner_tutorials::FibonacciFeedback feedback_;
    beginner_tutorials::FibonacciResult result_;

public:
    FibonacciAction(std::string name) :
        as_(nh_, name, boost::bind(&FibonacciAction::executeCB, this, _1), false),
        action_name_(name)
    {
        as_.start();
    }

    void executeCB(const beginner_tutorials::FibonacciGoalConstPtr &goal)
    {
        ros::Rate r(1);
        bool success = true;
        
        std::vector<int> sequence;
        sequence.push_back(0);
        sequence.push_back(1);
        
        feedback_.sequence = sequence;
        as_.publishFeedback(feedback_);
        
        for(int i=1; i<goal->order; i++)
        {
            if(as_.isPreemptRequested() || !ros::ok())
            {
                ROS_INFO("%s: 任务被取消", action_name_.c_str());
                as_.setPreempted();
                success = false;
                break;
            }
            
            sequence.push_back(sequence[i] + sequence[i-1]);
            feedback_.sequence = sequence;
            as_.publishFeedback(feedback_);
            r.sleep();
        }
        
        if(success)
        {
            result_.sequence = sequence;
            ROS_INFO("%s: 任务完成", action_name_.c_str());
            as_.setSucceeded(result_);
        }
    }
};

我的习惯:Action的反馈频率不要太高。我曾经设成100Hz反馈,结果网络带宽被占满,其他Topic都发不出去了。一般1-10Hz就够了,用户看起来是实时的。

4.5 自定义消息类型

ROS自带的消息类型(如String、Int32)够用吗?不够。在SLAM中,你需要传递点云、位姿、地图数据,这些都得自己定义。

4.5.1 定义.msg文件

// 文件: MySLAMData.msg
# 自定义SLAM数据消息
Header header
geometry_msgs/Point position    # 当前位置
geometry_msgs/Quaternion orientation  # 当前姿态
float32[] map_data              # 地图数据(一维数组)
int32 map_width                 # 地图宽度
int32 map_height                # 地图高度
float32 resolution              # 地图分辨率

4.5.2 在CMakeLists.txt中配置

find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  std_msgs
  geometry_msgs
  message_generation
)

add_message_files(
  FILES
  MySLAMData.msg
)

generate_messages(
  DEPENDENCIES
  std_msgs
  geometry_msgs
)

catkin_package(
  CATKIN_DEPENDS message_runtime
)

4.5.3 在package.xml中声明

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

避坑指南:我曾经在自定义消息里用了vector<vector<float>>,结果编译通过,运行时序列化报错。ROS的msg不支持嵌套动态数组。解决方案:用一维数组+长度字段,或者用自定义消息数组。

4.6 三种机制的选型对比

特性 Topic Service Action
通信模式 发布-订阅 请求-响应 目标-反馈-结果
实时性 高(异步) 中(同步阻塞) 高(异步+反馈)
适用场景 传感器数据、状态发布 参数查询、触发操作 导航、机械臂控制
是否支持取消
是否支持反馈

4.7 知识体系总览

下面这张图,是我梳理的ROS通信机制知识体系。你可以把它当作学习地图:

ROS通信机制知识体系 Topic(话题) Service(服务) Action(动作) Publisher / Subscriber 异步、多对多 传感器数据流 Server / Client 同步、一对一 触发式操作 Goal / Feedback / Result 异步、可取消 长时间任务 自定义消息类型(.msg / .srv / .action) 三种机制 + 自定义消息 = 完整的ROS通信体系 选对通信方式,系统性能提升50%

总结一下:Topic是ROS的血液,Service是ROS的神经反射,Action是ROS的肌肉运动。自定义消息类型,就是给这些通信机制定制“语言”。掌握了这些,你就能搭建出健壮的机器人系统。


公众号:蓝海资料掘金营,微信deep3321