【ROS2】ROS 2 中 TypeAdapter(类型适配器)的简介与使用

ROS2 2 次阅读
【ROS2】ROS 2 中 TypeAdapter(类型适配器)的简介与使用

1、示例代码

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

#include "rclcpp/type_adapter.hpp"
#include "rclcpp/rclcpp.hpp"

#include "std_msgs/msg/string.hpp"

using namespace std::chrono_literals;

template<>
struct rclcpp::TypeAdapter<std::string, std_msgs::msg::String>
{
  using is_specialized = std::true_type;
  using custom_type = std::string;
  using ros_message_type = std_msgs::msg::String;

  static
  void
  convert_to_ros_message(
    const custom_type & source,
    ros_message_type & destination)
  {
    destination.data = source;
  }

  static
  void
  convert_to_custom(
    const ros_message_type & source,
    custom_type & destination)
  {
    destination = source.data;
  }
};

class MinimalPublisher : public rclcpp::Node
{
  using MyAdaptedType = rclcpp::TypeAdapter<std::string, std_msgs::msg::String>;

public:
  MinimalPublisher()
  : Node("minimal_publisher"), count_(0)
  {
    publisher_ = this->create_publisher<MyAdaptedType>("topic", 10);
    timer_ = this->create_wall_timer(
      500ms, std::bind(&MinimalPublisher::timer_callback, this));
  }

private:
  void timer_callback()
  {
    std::string message = "Hello, world! " + std::to_string(count_++);
    RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.c_str());
    publisher_->publish(message);
  }
  rclcpp::TimerBase::SharedPtr timer_;
  rclcpp::Publisher<MyAdaptedType>::SharedPtr publisher_;
  size_t count_;
};

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

2、代码解析

这段代码是 ROS 2 中使用 TypeAdapter(类型适配器) 实现的极简发布者示例,核心目的是让开发者可以直接用 std::string 替代 ROS 2 标准的 std_msgs::msg::String 消息类型来发布数据,简化代码编写。本文将从整体功能、核心概念、逐模块解析、运行逻辑四个维度,对每一行的作用和背后的原理进行讲解。

2.1、整体功能总结

这个程序实现了一个 ROS 2 节点(minimal_publisher):

  1. 每隔 500ms 触发一次定时器回调;
  2. 每次回调生成一个包含计数的字符串(如 Hello, world! 0、Hello, world! 1...);
  3. 通过 ROS 2 的话题(topic)发布这个字符串;
  4. 核心特性:借助 TypeAdapter,发布时直接用 std::string,而非 ROS 2 原生的 std_msgs::msg::String 消息结构体,底层自动完成类型转换。

2.2、核心前置概念

在解析代码前,先理解两个关键概念:

  1. ROS 2 原生消息类型:比如 std_msgs::msg::String,是 ROS 2 定义的结构体,包含 data 成员(存储字符串),是话题通信的 “标准格式”;
  2. TypeAdapter(类型适配器):ROS 2 提供的工具,允许开发者用自定义类型(如 std::string)替代原生消息类型,底层自动完成 “自定义类型 ↔ 原生消息类型” 的转换,简化代码。

2.3、逐模块代码解析

  1. 头文件引入(基础依赖)
#include <chrono>       // 时间相关(如 500ms 字面量)
#include <functional>   // 绑定函数(std::bind)
#include <memory>       // 智能指针(SharedPtr)
#include <string>       // 标准字符串

#include "rclcpp/type_adapter.hpp"  // TypeAdapter 核心头文件
#include "rclcpp/rclcpp.hpp"        // ROS 2 核心 API(节点、发布者、定时器等)

#include "std_msgs/msg/string.hpp"  // ROS 2 标准字符串消息类型
  • rclcpp/type_adapter.hpp 是本次示例的核心,必须引入才能使用类型适配功能;
  • rclcpp/rclcpp.hpp 是 ROS 2 C++ 开发的 “万能头文件”,包含节点、发布者、日志等核心功能;
  • std_msgs/msg/string.hpp 引入 ROS 2 原生的字符串消息类型。
  1. 时间字面量命名空间
using namespace std::chrono_literals;
  • 启用 C++14 时间字面量(如 500ms),避免写 std::chrono::milliseconds(500),简化代码。
  1. TypeAdapter 特化(核心!类型适配逻辑)
// 特化 TypeAdapter,将 std::string 适配为 std_msgs::msg::String
template<>
struct rclcpp::TypeAdapter<std::string, std_msgs::msg::String>
{
  // 标记这是一个有效的特化(必须)
  using is_specialized = std::true_type;
  // 自定义类型(开发者想用的类型)
  using custom_type = std::string;
  // ROS 2 原生消息类型(被适配的类型)
  using ros_message_type = std_msgs::msg::String;

  // 自定义类型 → ROS 原生消息类型(发布时调用)
  static
  void
  convert_to_ros_message(
    const custom_type & source,  // 输入:std::string
    ros_message_type & destination)  // 输出:std_msgs::msg::String
  {
    destination.data = source;  // 核心转换:把字符串赋值给消息的 data 成员
  }

  // ROS 原生消息类型 → 自定义类型(订阅时调用,本例未用到,但必须实现)
  static
  void
  convert_to_custom(
    const ros_message_type & source,  // 输入:std_msgs::msg::String
    custom_type & destination)  // 输出:std::string
  {
    destination = source.data;  // 核心转换:提取消息的 data 成员到字符串
  }
};

这是整个示例的核心,作用是告诉 ROS 2:

  • 当我用 std::string 时,底层要自动转换成 std_msgs::msg::String;
  • 转换规则:std::string 直接赋值给 std_msgs::msg::String.data,反之亦然;
  • 注:即使本例只用到发布(convert_to_ros_message),也必须实现 convert_to_custom(订阅用),因为 TypeAdapter 要求完整的双向转换。
  1. 发布者类定义(MinimalPublisher)
class MinimalPublisher : public rclcpp::Node
{
  // 定义适配类型别名,简化后续使用(等价于 TypeAdapter<std::string, std_msgs::msg::String>)
  using MyAdaptedType = rclcpp::TypeAdapter<std::string, std_msgs::msg::String>;

public:
  // 构造函数:初始化节点、计数器、发布者、定时器
  MinimalPublisher()
  : Node("minimal_publisher"),  // 初始化父类(Node),节点名:minimal_publisher
    count_(0)  // 初始化计数器为 0
  {
    // 创建发布者:
    // 模板参数:适配类型 MyAdaptedType
    // 话题名:"topic",队列大小:10(消息积压最多存 10 条)
    publisher_ = this->create_publisher<MyAdaptedType>("topic", 10);
  
    // 创建定时器:
    // 周期:500ms,回调函数:绑定到 timer_callback 方法
    timer_ = this->create_wall_timer(
      500ms, std::bind(&MinimalPublisher::timer_callback, this));
  }

private:
  // 定时器回调函数(核心业务逻辑)
  void timer_callback()
  {
    // 生成发布消息:拼接字符串 + 计数器
    std::string message = "Hello, world! " + std::to_string(count_++);
    // 打印日志(ROS 2 标准日志,级别:INFO)
    RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.c_str());
    // 发布消息:直接传 std::string(TypeAdapter 自动转成 std_msgs::msg::String)
    publisher_->publish(message);
  }

  // 成员变量:
  rclcpp::TimerBase::SharedPtr timer_;          // 定时器智能指针
  rclcpp::Publisher<MyAdaptedType>::SharedPtr publisher_;  // 发布者智能指针
  size_t count_;  // 消息计数器(无符号整数)
};

关键点解析:

  • MyAdaptedType 是类型别名,避免重复写冗长的 TypeAdapter 模板;
  • create_publisher:创建基于适配类型的发布者;
  • publisher_->publish(message):直接发布 std::string,而非 std_msgs::msg::String(这是 TypeAdapter 的核心价值);
  • RCLCPP_INFO:ROS 2 日志宏,打印发布的消息内容,方便调试。
  1. 主函数(程序入口)
int main(int argc, char * argv[])
{
  // 初始化 ROS 2 上下文(必须)
  rclcpp::init(argc, argv);
  // 创建发布者节点的智能指针,并进入自旋(阻塞等待回调/消息)
  rclcpp::spin(std::make_shared<MinimalPublisher>());
  // 关闭 ROS 2 上下文(自旋退出后执行)
  rclcpp::shutdown();
  return 0;
}

ROS 2 程序的标准入口逻辑:

  • rclcpp::init:初始化 ROS 2 运行时(解析命令行参数、创建上下文等);
  • rclcpp::spin:让节点进入 “循环等待” 状态,处理定时器回调、消息收发等(阻塞函数,直到节点关闭);
  • rclcpp::shutdown:释放 ROS 2 资源,优雅退出。

2.4、运行逻辑梳理(完整流程)

  1. 程序启动 → main 函数调用 rclcpp::init 初始化 ROS 2;
  2. 创建 MinimalPublisher 节点:
    • 节点命名为 minimal_publisher;
    • 创建话题为 topic 的发布者(适配类型);
    • 创建 500ms 周期的定时器,绑定 timer_callback;
  3. rclcpp::spin 启动节点循环,定时器开始工作;
  4. 每 500ms 触发 timer_callback:
    • 生成 Hello, world! + 计数 的字符串;
    • 打印日志;
    • 调用 publish(message),TypeAdapter 自动把 std::string 转成 std_msgs::msg::String 并发布;
  5. 节点持续运行,直到用户按下 Ctrl+C,rclcpp::spin 退出,调用 rclcpp::shutdown 释放资源。

2.5、对比:不用 TypeAdapter 的等价代码

为了凸显 TypeAdapter 的优势,对比一下不用适配器的写法(发布部分):

// 不用 TypeAdapter 时,必须构造 std_msgs::msg::String
void timer_callback()
{
  std_msgs::msg::String msg;  // 必须创建原生消息对象
  msg.data = "Hello, world! " + std::to_string(count_++);  // 赋值给 data 成员
  RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", msg.data.c_str());
  publisher_->publish(msg);  // 发布原生消息对象
}

可以看到:TypeAdapter 省去了手动创建 std_msgs::msg::String 的步骤,直接用 std::string 发布,代码更简洁。

2.6、小结

  1. 核心功能:实现 ROS 2 周期性发布字符串话题,借助 TypeAdapter 简化类型使用;
  2. TypeAdapter 核心作用:定义 std::string ↔ std_msgs::msg::String 的自动转换规则,让开发者直接用原生 C++ 类型替代 ROS 消息类型;
  3. 关键流程:初始化节点 → 创建发布者 / 定时器 → 定时器回调生成并发布字符串 → 自旋等待回调。

3、技术背景与应用场景

3.1、TypeAdapter 特性的推出背景与版本

TypeAdapter(类型适配器)是 ROS 2 Humble Hawksbill (2022 LTS) 版本中正式推出的核心特性(首次在 ROS 2 Iron Irwini 版本中完善,后回溯到 Humble),属于 ROS 2 核心库 rclcpp 的增强功能。 推出的核心动机: ROS 2 原生消息类型(如 std_msgs::msg::String、geometry_msgs::msg::Pose)本质是自定义结构体,开发者在业务代码中需要频繁在 “原生 C++ 类型(如 std::string、Eigen::Vector3d)” 和 “ROS 消息类型” 之间手动转换,代码冗余且易出错。TypeAdapter 正是为了解决这个痛点,实现类型转换的 “自动化” 和 “统一化”。

3.2、TypeAdapter 的核心适用场景

TypeAdapter 本质是 “自定义类型 ↔ ROS 2 原生消息类型” 的双向转换桥梁,以下是它最实用的 5 个场景:

  1. 简化基础类型与 ROS 消息的转换(最常用)
    • 场景:用原生 C++ 类型替代 ROS 基础消息类型(如 std::string 替代 std_msgs::msg::String、int 替代 std_msgs::msg::Int32)。
    • 优势:省去手动创建 ROS 消息对象、赋值 / 提取 data 成员的冗余代码。
    • 示例:
// 不用 TypeAdapter:手动构造消息
std_msgs::msg::Int32 msg;
msg.data = 100;
publisher->publish(msg);

// 用 TypeAdapter:直接发布 int 类型
publisher->publish(100); // 底层自动转 std_msgs::msg::Int32
  1. 适配第三方库类型(如 Eigen、OpenCV)
    • 场景:ROS 2 中常用 Eigen(线性代数)、OpenCV(图像处理)等库,这些库的类型(如 Eigen::Vector3d、cv::Mat)与 ROS 消息类型(如 geometry_msgs::msg::Point、sensor_msgs::msg::Image)需要频繁转换。
    • 优势:将转换逻辑封装到 TypeAdapter 中,业务代码无需关注转换细节,降低耦合。
    • 示例:适配 Eigen::Vector3d 到 geometry_msgs::msg::Point:
template<>
struct rclcpp::TypeAdapter<Eigen::Vector3d, geometry_msgs::msg::Point>
{
  using is_specialized = std::true_type;
  using custom_type = Eigen::Vector3d;
  using ros_message_type = geometry_msgs::msg::Point;

  static void convert_to_ros_message(const custom_type& src, ros_message_type& dst) {
    dst.x = src.x();
    dst.y = src.y();
    dst.z = src.z();
  }

  static void convert_to_custom(const ros_message_type& src, custom_type& dst) {
    dst = Eigen::Vector3d(src.x, src.y, src.z);
  }
};

// 业务代码中直接用 Eigen::Vector3d 发布/订阅
publisher->publish(Eigen::Vector3d(1.0, 2.0, 3.0));
  1. 封装自定义业务类型(简化复杂消息)
    • 场景:项目中有自定义的业务结构体(如 RobotPose),需要映射到 ROS 复合消息(如 geometry_msgs::msg::PoseStamped)。
    • 优势:业务代码只用自定义结构体,ROS 消息转换完全交给 TypeAdapter,代码更易维护。
    • 示例:
// 自定义业务类型
struct RobotPose {
  double x, y, theta;
  std::string frame_id;
};

// 适配到 ROS 的 PoseStamped 消息
template<>
struct rclcpp::TypeAdapter<RobotPose, geometry_msgs::msg::PoseStamped> {
  // 实现转换逻辑:RobotPose → PoseStamped 反之亦然
};

// 发布时直接传 RobotPose
publisher->publish(RobotPose{1.0, 2.0, 0.5, "base_link"});
  1. 统一多模块的类型转换逻辑
    • 场景:大型项目中,多个节点 / 模块都需要处理 “同一种自定义类型 ↔ 同一种 ROS 消息” 的转换(如所有模块都用 Eigen::Quaterniond 表示姿态)。
    • 优势:将转换逻辑集中在 TypeAdapter 特化结构体中(通常放在公共头文件),避免每个模块重复写转换代码,减少维护成本。
  2. 兼容旧代码 / 第三方组件
    • 场景:项目中已有大量使用原生 C++ 类型的代码,需要接入 ROS 2 但不想大幅修改原有逻辑。
    • 优势:通过 TypeAdapter 适配原有类型到 ROS 消息,原有业务代码几乎无需改动,仅需修改发布 / 订阅的模板参数。

3.3、TypeAdapter 的使用限制(新手需注意)

  1. 仅支持 C++:TypeAdapter 是 rclcpp 的特性,ROS 2 Python 中无对应实现(Python 本身动态类型,无需此机制);
  2. 必须实现双向转换:即使只用到 “自定义→ROS”(发布),也必须实现 “ROS→自定义”(订阅),否则编译报错;
  3. 仅适配消息类型:只能用于话题 / 服务 / 动作的消息类型转换,不能用于参数、日志等其他场景;
  4. LTS 版本兼容性:仅 Humble 及以上版本支持(Iron、Jazzy、Kilted 均兼容),Foxy 及更早版本无此特性。

3.4、小结

  1. TypeAdapter 是 ROS 2 Humble (2022 LTS) 推出的特性,核心解决 “自定义类型 ↔ ROS 原生消息类型” 的手动转换痛点;
  2. 核心适用场景:简化基础类型转换、适配第三方库类型(Eigen/OpenCV)、封装自定义业务类型、统一多模块转换逻辑、兼容旧代码;
  3. 关键优势:减少冗余转换代码、降低业务逻辑与 ROS 消息的耦合,让开发者更聚焦业务本身。