欢迎光临
我们一直在努力

ROS2 C++ 回调函数 保姆级教程

目录

摘要

一、回调函数到底是什么?

1.1 普通函数是主动调用

1.2 回调函数是被动调用

1.3 ROS2 为什么大量使用回调函数?

1.4 回调函数和 rclcpp::spin() 的关系

二、Topic 订阅回调函数详解

2.1 Topic 回调函数的作用

2.2 订阅 /cmd_vel 的完整代码

2.3 这段代码的执行流程

2.4 SharedPtr msg 是什么意思?

2.5 为什么是 msg->linear.x?

三、Timer 定时器回调函数详解

3.1 Timer 回调函数的作用

3.2 定时发布 /cmd_vel 的完整代码

3.3 Timer 回调函数什么时候执行?

3.4 Timer 回调为什么没有参数?

四、Service 和 Action 中的回调函数

4.1 Service 服务回调函数的作用

4.2 自定义服务接口示例

4.3 Service 回调函数代码示例

4.4 Service 回调函数什么时候触发?

4.5 Action 中的回调函数简单理解

五、std::bind、this、_1、_2 和常见问题总结

5.1 std::bind 是什么?

5.2 &LimoTopicSub::cmd_callback 是什么意思?

5.3 this 是什么意思?

5.4 _1 和 _2 是什么意思?

5.5 Timer 为什么不需要 _1?

5.6 也可以使用 Lambda 表达式写回调

5.7 常见错误总结

(1)忘记写 rclcpp::spin(node)

(2)Topic 回调参数类型写错

(3)忘记包含

(4)指针访问成员时写成了 .

(5)Service 回调少写 _2

六、总结


摘要

在 ROS2 C++ 开发中,回调函数是一个非常核心的概念。

无论是订阅话题、定时发布消息、处理服务请求,还是执行 Action 任务,都会大量用到回调函数。

很多初学者刚开始学习 ROS2 时,经常会看到下面这种代码:

std::bind(&LimoTopicSub::cmd_callback, this, std::placeholders::_1)

或者:

this->create_wall_timer(
500ms,
std::bind(&LimoTopicCmd::timer_callback, this)
);

看起来比较复杂,其实本质并不难。

一句话理解:

回调函数就是提前写好的函数,等某个事件发生时,由 ROS2 自动帮我们调用。

本篇文章就围绕 ROS2 C++ 中的回调函数展开,结合 Topic、Timer、Service、Action 几种常见场景,详细讲清楚:

  • 回调函数是什么
  • 什么时候会触发
  • std::bind 是什么
  • this 是什么
  • std::placeholders::_1 又是什么意思

  • 一、回调函数到底是什么?

    1.1 普通函数是主动调用

    先看一个普通函数:

    #include <iostream>

    void say_hello()
    {
    std::cout << "Hello ROS2" << std::endl;
    }

    int main()
    {
    say_hello();
    return 0;
    }

    这段代码中,say_hello() 是我们在 main() 函数中主动调用的。

    执行流程是:

    main 函数开始执行
    ↓
    主动调用 say_hello()
    ↓
    say_hello() 执行
    ↓
    程序结束

    也就是说:

    普通函数一般是我们自己主动调用。


    1.2 回调函数是被动调用

    回调函数不一样。

    回调函数通常不是我们自己直接调用,而是提前把函数交给 ROS2,当某个事件发生时,ROS2 自动帮我们调用这个函数。

    例如订阅 /cmd_vel 话题:

    sub_ = this->create_subscription<geometry_msgs::msg::Twist>(
    "/cmd_vel",
    10,
    std::bind(&LimoTopicSub::cmd_callback, this, std::placeholders::_1)
    );

    这里的 cmd_callback() 并不是我们自己手动调用的。

    它的触发条件是:

    /cmd_vel 话题收到新消息
    ↓
    ROS2 自动调用 cmd_callback()
    ↓
    回调函数解析收到的消息

    所以可以这样理解:

  • 普通函数:我主动调用函数
  • 回调函数:事件发生后,系统自动调用函数

  • 1.3 ROS2 为什么大量使用回调函数?

    ROS2 是机器人通信框架。

    机器人程序和普通程序不太一样。

    1. 普通程序很多时候是从上往下执行一遍:

    第一步
    第二步
    第三步
    程序结束

    2. 但是机器人程序需要一直运行,并且随时响应各种事件。

    例如:

    有没有新的速度指令?
    有没有新的雷达数据?
    有没有客户端发送服务请求?
    有没有新的 Action 目标?
    定时器时间到了没有?

    这些事件什么时候发生是不固定的。

    所以 ROS2 采用了事件驱动的方式:

  • 收到话题消息        →   触发 Topic 回调函数
  • 定时器到时间        →   触发 Timer 回调函数
  • 收到服务请求        →   触发 Service 回调函数
  • 收到 Action 目标  →   触发 Action 回调函数
  • 这就是 ROS2 中回调函数非常重要的原因。


    1.4 回调函数和 rclcpp::spin() 的关系

    很多初学者写完回调函数后,发现程序没有反应。

    其中一个常见原因就是没有写:

    rclcpp::spin(node);

    一个完整的 ROS2 节点主函数一般是这样:

    int main(int argc, char * argv[])
    {
    rclcpp::init(argc, argv);

    auto node = std::make_shared<LimoTopicSub>();

    rclcpp::spin(node);

    rclcpp::shutdown();

    return 0;
    }

    其中:

    rclcpp::spin(node);

    可以理解为:

    让节点一直运行,并且不断检查有没有事件需要处理。

    比如:

    有没有新的 Topic 消息?
    有没有 Timer 到时间?
    有没有 Service 请求?
    有没有 Action 目标?

    如果有事件发生,ROS2 就会自动调用对应的回调函数。

    所以:

    1. 没有 spin:     节点创建完可能就结束了,回调函数没有机会执行。

    2. 有 spin:     节点持续运行,等待事件发生,然后触发回调函数。


    二、Topic 订阅回调函数详解

    2.1 Topic 回调函数的作用

    Topic 订阅回调函数的作用是:

    当订阅的话题收到新消息时,自动执行对应的处理逻辑。

    例如小车控制中经常使用的话题:

    /cmd_vel

    消息类型是:

    geometry_msgs/msg/Twist

    这个话题通常用来发送小车速度指令。


    2.2 订阅 /cmd_vel 的完整代码

    下面是一个 ROS2 C++ 订阅者节点,用来订阅 /cmd_vel 话题。

    #include <memory>
    #include <functional>

    #include "rclcpp/rclcpp.hpp"
    #include "geometry_msgs/msg/twist.hpp"

    using std::placeholders::_1;

    class LimoTopicSub : public rclcpp::Node
    {
    public:
    LimoTopicSub() : Node("limo_topic_sub")
    {
    cmd_sub_ = this->create_subscription<geometry_msgs::msg::Twist>(
    "/cmd_vel",
    10,
    std::bind(&LimoTopicSub::cmd_callback, this, _1)
    );

    RCLCPP_INFO(this->get_logger(), "limo_topic_sub node has started.");
    }

    private:
    void cmd_callback(const geometry_msgs::msg::Twist::SharedPtr msg)
    {
    RCLCPP_INFO(this->get_logger(),
    "收到速度指令:linear.x = %.2f, angular.z = %.2f",
    msg->linear.x,
    msg->angular.z);
    }

    rclcpp::Subscription<geometry_msgs::msg::Twist>::SharedPtr cmd_sub_;
    };

    int main(int argc, char * argv[])
    {
    rclcpp::init(argc, argv);

    auto node = std::make_shared<LimoTopicSub>();

    rclcpp::spin(node);

    rclcpp::shutdown();

    return 0;
    }


    2.3 这段代码的执行流程

    代码运行后,整体流程如下:

    程序启动
    ↓
    创建 LimoTopicSub 节点
    ↓
    执行构造函数 LimoTopicSub()
    ↓
    创建 /cmd_vel 订阅者
    ↓
    打印:limo_topic_sub node has started.
    ↓
    进入 rclcpp::spin(node)
    ↓
    等待 /cmd_vel 话题消息
    ↓
    一旦收到 Twist 消息
    ↓
    ROS2 自动调用 cmd_callback()
    ↓
    打印 linear.x 和 angular.z

    所以如果一直没有任何节点往 /cmd_vel 发布消息,终端通常只会看到:

    [INFO] [limo_topic_sub]: limo_topic_sub node has started.

    然后程序不会结束,而是一直卡在这里等待:

    rclcpp::spin(node);

    因此:

    cmd_callback()

    不需要我们自己手动调用。

    只要 /cmd_vel 收到新消息,ROS2 就会自动调用它。


    2.4 SharedPtr msg 是什么意思?

    回调函数中有这样一行:

    void cmd_callback(const geometry_msgs::msg::Twist::SharedPtr msg)

    可以拆开理解:

    geometry_msgs::msg::Twist

    表示消息类型是 Twist。

    SharedPtr

    表示智能指针。

    msg

    表示变量名。

    所以这一行整体意思是:

  • 定义一个回调函数 cmd_callback
  • 它接收一个 Twist 类型的消息指针
  • 这个消息指针的名字叫 msg
  • 其中 msg 只是变量名,可以修改。

    例如:

    void cmd_callback(const geometry_msgs::msg::Twist::SharedPtr data)
    {
    RCLCPP_INFO(this->get_logger(), "linear.x = %.2f", data->linear.x);
    }

    这样写也是可以的。

    只不过 ROS2 代码中习惯写成 msg,因为它一看就知道代表接收到的消息。


    2.5 为什么是 msg->linear.x?

    因为 msg 是指针,所以访问里面的成员变量时要用:

    msg->linear.x
    msg->angular.z

    而不是:

    msg.linear.x
    msg.angular.z

    简单记:

    普通对象访问成员:用 .
    指针访问成员:用 ->

    所以:

    msg->linear.x

    可以理解为:

    访问 msg 指向的 Twist 消息里面的 linear.x 字段


    三、Timer 定时器回调函数详解

    3.1 Timer 回调函数的作用

    Timer 定时器回调函数用于周期性执行某个任务。

    例如:

  • 每 500ms 发布一次速度指令
  • 每 1 秒打印一次状态
  • 每 100ms 读取一次传感器数据
  • 每隔一段时间检查一次任务状态
  • 在 ROS2 小车控制中,经常会用 Timer 定时发布 /cmd_vel 速度指令。


    3.2 定时发布 /cmd_vel 的完整代码

    下面这个节点会每隔 500ms 执行一次 timer_callback(),并向 /cmd_vel 发布速度消息。

    #include <memory>
    #include <functional>
    #include <chrono>

    #include "rclcpp/rclcpp.hpp"
    #include "geometry_msgs/msg/twist.hpp"

    using namespace std::chrono_literals;

    class LimoTopicCmd : public rclcpp::Node
    {
    public:
    LimoTopicCmd() : Node("limo_topic_cmd"), count_(0)
    {
    pub_vel_ = this->create_publisher<geometry_msgs::msg::Twist>(
    "/cmd_vel",
    10
    );

    timer_ = this->create_wall_timer(
    500ms,
    std::bind(&LimoTopicCmd::timer_callback, this)
    );

    RCLCPP_INFO(this->get_logger(), "limo_topic_cmd node has started.");
    }

    private:
    void timer_callback()
    {
    geometry_msgs::msg::Twist vel_cmd;

    if (count_ < 4)
    {
    vel_cmd.linear.x = 0.1;
    vel_cmd.angular.z = 0.0;

    RCLCPP_INFO(this->get_logger(), "小车前进中…");
    }
    else
    {
    vel_cmd.linear.x = 0.0;
    vel_cmd.angular.z = 0.0;

    RCLCPP_INFO(this->get_logger(), "小车停止。");
    }

    pub_vel_->publish(vel_cmd);

    count_++;
    }

    rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr pub_vel_;
    rclcpp::TimerBase::SharedPtr timer_;

    int count_;
    };

    int main(int argc, char * argv[])
    {
    rclcpp::init(argc, argv);

    auto node = std::make_shared<LimoTopicCmd>();

    rclcpp::spin(node);

    rclcpp::shutdown();

    return 0;
    }


    3.3 Timer 回调函数什么时候执行?

    核心代码是:

    timer_ = this->create_wall_timer(
    500ms,
    std::bind(&LimoTopicCmd::timer_callback, this)
    );

    这段代码的意思是:

  • 创建一个定时器
  • 每隔 500ms
  • 自动调用一次 timer_callback()
  • 执行流程如下:

    启动 limo_topic_cmd 节点
    ↓
    创建 /cmd_vel 发布者
    ↓
    创建 500ms 定时器
    ↓
    进入 rclcpp::spin(node)
    ↓
    每隔 500ms 自动触发 timer_callback()
    ↓
    发布一次 Twist 速度消息

    所以 Timer 回调函数的特点是:

  • 不需要外部发送消息
  • 只要时间到了,就自动执行

  • 3.4 Timer 回调为什么没有参数?

    Topic 订阅回调一般有参数:

    void cmd_callback(const geometry_msgs::msg::Twist::SharedPtr msg)

    因为 Topic 回调需要接收外部传来的消息。

    但是 Timer 回调一般没有参数:

    void timer_callback()

    因为它只是时间到了自动执行,不需要 ROS2 额外传入消息。

    所以可以这样区分:

  • Topic 回调:收到消息后触发,通常有 msg 参数
  • Timer 回调:定时时间到了触发,通常没有参数

  • 四、Service 和 Action 中的回调函数

    4.1 Service 服务回调函数的作用

    Service 是一种请求 / 响应通信方式。

    它的通信流程是:

    客户端发送请求
    ↓
    服务端处理请求
    ↓
    服务端返回响应

    在服务端中,负责处理请求的函数就是 Service 回调函数。

    例如我们定义一个小车控制服务:

    客户端发送 x、y、z
    服务端收到后发布 /cmd_vel
    最后返回 success


    4.2 自定义服务接口示例

    假设自定义服务文件为:

    limo_msgs/srv/LimoSrv.srv

    内容如下:

    float32 x
    float32 y
    float32 z
    —
    bool success

    其中:

    — 上面是请求 Request
    — 下面是响应 Response

    也就是说:

    客户端发送:

    x
    y
    z

    服务端返回:

    success


    4.3 Service 回调函数代码示例

    #include <memory>
    #include <functional>

    #include "rclcpp/rclcpp.hpp"
    #include "geometry_msgs/msg/twist.hpp"
    #include "limo_msgs/srv/limo_srv.hpp"

    using std::placeholders::_1;
    using std::placeholders::_2;

    class LimoSrvServer : public rclcpp::Node
    {
    public:
    LimoSrvServer() : Node("limo_srv_server")
    {
    cmd_pub_ = this->create_publisher<geometry_msgs::msg::Twist>(
    "/cmd_vel",
    10
    );

    srv_ = this->create_service<limo_msgs::srv::LimoSrv>(
    "/limo_srv",
    std::bind(&LimoSrvServer::srv_callback, this, _1, _2)
    );

    RCLCPP_INFO(this->get_logger(), "limo_srv_server node has started.");
    }

    private:
    void srv_callback(
    const std::shared_ptr<limo_msgs::srv::LimoSrv::Request> request,
    std::shared_ptr<limo_msgs::srv::LimoSrv::Response> response)
    {
    geometry_msgs::msg::Twist vel_cmd;

    vel_cmd.linear.x = request->x;
    vel_cmd.linear.y = request->y;
    vel_cmd.angular.z = request->z;

    cmd_pub_->publish(vel_cmd);

    response->success = true;

    RCLCPP_INFO(this->get_logger(),
    "收到服务请求:x = %.2f, y = %.2f, z = %.2f",
    request->x,
    request->y,
    request->z);
    }

    rclcpp::Publisher<geometry_msgs::msg::Twist>::SharedPtr cmd_pub_;
    rclcpp::Service<limo_msgs::srv::LimoSrv>::SharedPtr srv_;
    };

    int main(int argc, char * argv[])
    {
    rclcpp::init(argc, argv);

    auto node = std::make_shared<LimoSrvServer>();

    rclcpp::spin(node);

    rclcpp::shutdown();

    return 0;
    }


    4.4 Service 回调函数什么时候触发?

    这一行代码创建了服务端:

    srv_ = this->create_service<limo_msgs::srv::LimoSrv>(
    "/limo_srv",
    std::bind(&LimoSrvServer::srv_callback, this, _1, _2)
    );

    意思是:

  • 创建一个 /limo_srv 服务
  • 当客户端调用 /limo_srv 时
  • 自动执行 srv_callback()
  • 执行流程如下:

    服务端启动
    ↓
    创建 /limo_srv 服务
    ↓
    进入 rclcpp::spin(node)
    ↓
    客户端发送请求
    ↓
    ROS2 自动调用 srv_callback()
    ↓
    服务端读取 request
    ↓
    服务端填写 response
    ↓
    结果返回给客户端


    4.5 Action 中的回调函数简单理解

    Action 适合处理长时间任务。

    例如:

    导航到目标点
    巡检一段路线
    让小车持续运动一段时间
    机械臂执行抓取任务

    Action 和 Service 的区别是:

  • Service:一次请求,一次响应
  • Action:发送目标,持续反馈,最终返回结果
  • Action 服务端常见回调函数有:

  • handle_goal:收到目标时触发
  • handle_cancel:收到取消请求时触发
  • handle_accepted:目标被接受后触发
  • execute:真正执行任务的函数
  • Action 客户端常见回调函数有:

  • goal_response_callback:服务端是否接受目标
  • feedback_callback:接收执行过程反馈
  • result_callback:接收最终执行结果
  • 所以 Action 中的回调更多,是因为 Action 需要处理完整的任务生命周期。

    可以简单理解为:

  • Topic 回调:收到消息就触发
  • Timer 回调:时间到了就触发
  • Service 回调:客户端请求来了就触发
  • Action 回调:目标、反馈、结果等不同阶段都会触发

  • 五、std::bind、this、_1、_2 和常见问题总结

    5.1 std::bind 是什么?

    在 ROS2 C++ 中,经常会看到这种写法:

    std::bind(&LimoTopicSub::cmd_callback, this, _1)

    它的作用是:

    把类里面的成员函数绑定成 ROS2 可以调用的回调函数。

  • 因为 cmd_callback() 是类 LimoTopicSub 里面的成员函数。
  • 成员函数不能像普通函数一样直接传给 ROS2。
  • 所以需要告诉 ROS2:

    这个回调函数属于哪个类?
    用哪个对象来调用?
    将来要接收几个参数?

    std::bind 就是用来完成这件事的。


    5.2 &LimoTopicSub::cmd_callback 是什么意思?

    这一部分:

    &LimoTopicSub::cmd_callback

    表示:

    取出 LimoTopicSub 类中的 cmd_callback 成员函数地址

    可以理解为:

    我要把 LimoTopicSub 这个类里面的 cmd_callback 函数交给 ROS2


    5.3 this 是什么意思?

    这一部分:

    this

    表示当前对象。

    因为 cmd_callback() 是类里面的成员函数,它必须依赖某一个具体对象才能调用。

    例如:

    std::bind(&LimoTopicSub::cmd_callback, this, _1)

    可以理解为:

    使用当前这个 LimoTopicSub 节点对象
    去调用它自己的 cmd_callback 函数

    也就是说,this 指向当前节点对象。


    5.4 _1 和 _2 是什么意思?

    _1 完整写法是:

    std::placeholders::_1

    它是一个占位符。

    意思是:

  • 这里先占一个位置
  • 等 ROS2 真正调用回调函数时
  • 把第 1 个参数传进来
  • 例如 Topic 回调函数:

    void cmd_callback(const geometry_msgs::msg::Twist::SharedPtr msg)

    这个函数需要一个参数 msg。

    所以绑定时写:

    std::bind(&LimoTopicSub::cmd_callback, this, _1)

    意思是:

  • ROS2 收到消息后
  • 把收到的消息作为第 1 个参数
  • 传给 cmd_callback()
  • Service 回调函数一般有两个参数:

    request
    response

    所以绑定时写:

    std::bind(&LimoSrvServer::srv_callback, this, _1, _2)

    其中:

    _1 对应 request
    _2 对应 response

    也就是:

    客户端请求来了
    ↓
    ROS2 把请求数据放到 _1
    ↓
    ROS2 把响应对象放到 _2
    ↓
    调用 srv_callback(request, response)


    5.5 Timer 为什么不需要 _1?

    Timer 回调函数一般没有参数:

    void timer_callback()

    所以绑定时不需要 _1:

    std::bind(&LimoTopicCmd::timer_callback, this)

    因为 Timer 回调不是接收消息,也不是处理请求。

    它只是:

    时间到了
    ↓
    自动执行 timer_callback()

    所以不需要 ROS2 传入额外参数。


    5.6 也可以使用 Lambda 表达式写回调

    除了 std::bind,ROS2 C++ 中也可以使用 Lambda 表达式。

    Topic 回调可以写成:

    ```cpp
    cmd_sub_ = this->create_subscription<geometry_msgs::msg::Twist>(
    "/cmd_vel",
    10,
    [this](const geometry_msgs::msg::Twist::SharedPtr msg)
    {
    this->cmd_callback(msg);
    }
    );
    ```

    对比:

    cmd_sub_ = this->create_subscription<geometry_msgs::msg::Twist>(
    "/cmd_vel",
    10,
    std::bind(&LimoTopicSub::cmd_callback, this, _1)

    Timer 回调可以写成:

    ```cpp
    timer_ = this->create_wall_timer(
    500ms,
    [this]()
    {
    this->timer_callback();
    }
    );
    ```

    对比:

    timer_ = this->create_wall_timer(
    500ms,
    std::bind(&LimoTopicCmd::timer_callback, this)
    );

    这两种写法本质上都可以。

    对初学者来说:

    std::bind 写法:ROS2 官方示例中比较常见
    Lambda 写法:看起来更直观,参数更清楚

    实际项目中,两种写法都能用。


    5.7 常见错误总结

    (1)忘记写 rclcpp::spin(node)

    如果没有写:

    rclcpp::spin(node);

    节点可能创建完就结束,回调函数不会被触发。


    (2)Topic 回调参数类型写错

    订阅 /cmd_vel 时,消息类型是:

    geometry_msgs::msg::Twist

    那么回调函数参数也要对应:

    void cmd_callback(const geometry_msgs::msg::Twist::SharedPtr msg)

    如果类型不一致,就会编译报错。


    (3)忘记包含 <functional>

    如果使用了:

    std::bind
    std::placeholders::_1

    就需要包含头文件:

    #include <functional>

    否则可能会出现 std::bind 未定义的问题。


    (4)指针访问成员时写成了 .

    错误写法:

    msg.linear.x

    正确写法:

    msg->linear.x

    因为 msg 是指针,所以要用 ->。


    (5)Service 回调少写 _2

    Service 回调一般有两个参数:

    request
    response

    所以绑定时一般要写:

    std::bind(&LimoSrvServer::srv_callback, this, _1, _2)

    如果只写 _1,参数数量对不上,也会出错。


    六、总结

    本篇文章主要讲解了 ROS2 C++ 中非常重要的回调函数。

    回调函数可以简单理解为:

    提前写好的函数,等某个事件发生时,由 ROS2 自动调用。

    在 ROS2 中,常见的回调场景包括:

  • Topic 回调:收到话题消息时触发
  • Timer 回调:定时时间到了触发
  • Service 回调:客户端发送请求时触发
  • Action 回调:目标、反馈、结果等阶段触发
  • 其中:

    std::bind

    用于把类成员函数绑定成 ROS2 可以调用的回调函数。

    this

    表示当前节点对象。

    _1

    表示第一个参数。

    _2

    表示第二个参数。

    再简单总结一句:

    • Topic 收消息靠回调
    • Timer 定时执行靠回调
    • Service 处理请求靠回调
    • Action 管理任务过程也靠回调

    理解了回调函数,后面学习 ROS2 的 Publisher、Subscriber、Service、Action,就会轻松很多。

    赞(0)
    未经允许不得转载:171主机测评 » ROS2 C++ 回调函数 保姆级教程
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址