【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):
- 每隔 500ms 触发一次定时器回调;
- 每次回调生成一个包含计数的字符串(如 Hello, world! 0、Hello, world! 1...);
- 通过 ROS 2 的话题(topic)发布这个字符串;
- 核心特性:借助 TypeAdapter,发布时直接用 std::string,而非 ROS 2 原生的 std_msgs::msg::String 消息结构体,底层自动完成类型转换。
2.2、核心前置概念
在解析代码前,先理解两个关键概念:
- ROS 2 原生消息类型:比如 std_msgs::msg::String,是 ROS 2 定义的结构体,包含 data 成员(存储字符串),是话题通信的 “标准格式”;
- TypeAdapter(类型适配器):ROS 2 提供的工具,允许开发者用自定义类型(如 std::string)替代原生消息类型,底层自动完成 “自定义类型 ↔ 原生消息类型” 的转换,简化代码。
2.3、逐模块代码解析
- 头文件引入(基础依赖)
#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 原生的字符串消息类型。
- 时间字面量命名空间
using namespace std::chrono_literals;
- 启用 C++14 时间字面量(如 500ms),避免写 std::chrono::milliseconds(500),简化代码。
- 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 要求完整的双向转换。
- 发布者类定义(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 日志宏,打印发布的消息内容,方便调试。
- 主函数(程序入口)
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、运行逻辑梳理(完整流程)
- 程序启动 → main 函数调用 rclcpp::init 初始化 ROS 2;
- 创建 MinimalPublisher 节点:
- 节点命名为 minimal_publisher;
- 创建话题为 topic 的发布者(适配类型);
- 创建 500ms 周期的定时器,绑定 timer_callback;
- rclcpp::spin 启动节点循环,定时器开始工作;
- 每 500ms 触发 timer_callback:
- 生成 Hello, world! + 计数 的字符串;
- 打印日志;
- 调用 publish(message),TypeAdapter 自动把 std::string 转成 std_msgs::msg::String 并发布;
- 节点持续运行,直到用户按下 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、小结
- 核心功能:实现 ROS 2 周期性发布字符串话题,借助 TypeAdapter 简化类型使用;
- TypeAdapter 核心作用:定义 std::string ↔ std_msgs::msg::String 的自动转换规则,让开发者直接用原生 C++ 类型替代 ROS 消息类型;
- 关键流程:初始化节点 → 创建发布者 / 定时器 → 定时器回调生成并发布字符串 → 自旋等待回调。
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 个场景:
- 简化基础类型与 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
- 适配第三方库类型(如 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));
- 封装自定义业务类型(简化复杂消息)
- 场景:项目中有自定义的业务结构体(如 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"});
- 统一多模块的类型转换逻辑
- 场景:大型项目中,多个节点 / 模块都需要处理 “同一种自定义类型 ↔ 同一种 ROS 消息” 的转换(如所有模块都用 Eigen::Quaterniond 表示姿态)。
- 优势:将转换逻辑集中在 TypeAdapter 特化结构体中(通常放在公共头文件),避免每个模块重复写转换代码,减少维护成本。
- 兼容旧代码 / 第三方组件
- 场景:项目中已有大量使用原生 C++ 类型的代码,需要接入 ROS 2 但不想大幅修改原有逻辑。
- 优势:通过 TypeAdapter 适配原有类型到 ROS 消息,原有业务代码几乎无需改动,仅需修改发布 / 订阅的模板参数。
3.3、TypeAdapter 的使用限制(新手需注意)
- 仅支持 C++:TypeAdapter 是 rclcpp 的特性,ROS 2 Python 中无对应实现(Python 本身动态类型,无需此机制);
- 必须实现双向转换:即使只用到 “自定义→ROS”(发布),也必须实现 “ROS→自定义”(订阅),否则编译报错;
- 仅适配消息类型:只能用于话题 / 服务 / 动作的消息类型转换,不能用于参数、日志等其他场景;
- LTS 版本兼容性:仅 Humble 及以上版本支持(Iron、Jazzy、Kilted 均兼容),Foxy 及更早版本无此特性。
3.4、小结
- TypeAdapter 是 ROS 2 Humble (2022 LTS) 推出的特性,核心解决 “自定义类型 ↔ ROS 原生消息类型” 的手动转换痛点;
- 核心适用场景:简化基础类型转换、适配第三方库类型(Eigen/OpenCV)、封装自定义业务类型、统一多模块转换逻辑、兼容旧代码;
- 关键优势:减少冗余转换代码、降低业务逻辑与 ROS 消息的耦合,让开发者更聚焦业务本身。