首页/目录/全部文章

全部文章

八个专题的源码、算法与协议笔记都在这里。

笔记列表

rqt_srv 源码详细分析

rqt_srv 源码详细分析

工作区路径:/home/cp/work2/ros2Learn/ros2_humble/src/ros-visualization/rqt_srv
版本:1.0.3

rqt_srv 浏览 ROS service 类型定义(.srv 文件结构),与 rqt_msg 对称。


1. 结构

1
2
3
4
5
rqt_srv/
├── plugin.xml # Services → rqt_srv.services.Services
└── src/rqt_srv/
├── main.py
└── services.py

2. 功能

  • 列出 workspace 中 service 类型
  • 显示 Request/Response 字段树

3. 插件

1
2
<class name="Services" type="rqt_srv.services.Services"
base_class_type="rqt_gui_py::Plugin">

4. 小结

配合 rqt_service_caller:先看类型,再填字段调用。

rqt_topic 源码详细分析

rqt_topic 源码详细分析

工作区路径:/home/cp/work2/ros2Learn/ros2_humble/src/ros-visualization/rqt_topic
版本:1.5.1

rqt_topic 显示 话题调试信息:发布者/订阅者列表、消息类型、发布频率、Echo 等。


1. 结构

1
2
3
4
5
6
rqt_topic/
├── plugin.xml # TopicPlugin → rqt_topic.topic.Topic
├── src/rqt_topic/
│ ├── main.py
│ └── topic.py # Plugin 主类 Topic
└── test/

2. 功能

功能 实现要点
话题列表 rclpy graph + rqt_py_common
类型显示 get_topic_names_and_types
频率统计 订阅 sample 计算 Hz
Echo 可选显示消息内容

3. 插件

1
2
<class name="TopicPlugin" type="rqt_topic.topic.Topic"
base_class_type="rqt_gui_py::Plugin">

菜单:Plugins → Introspection → Topics


4. 依赖

rclpy, rqt_gui_py, rqt_py_common, python_qt_binding


5. 启动

1
ros2 run rqt_topic rqt_topic

6. 小结

ROS 2 版 rostopic GUI:排查「谁在 pub/sub、类型是否匹配」的首选工具。

rqt 源码详细分析

rqt 源码详细分析

工作区路径:/home/cp/work2/ros2Learn/ros2_humble/src/ros-visualization/rqt
版本:1.1.9(子包统一版本)。

rqt 目录含 ROS 2 版 rqt 框架 5 个包:meta 包 rqt、GUI 入口 rqt_gui、Python/C++ 插件桥 rqt_gui_py / rqt_gui_cpp、公共工具 rqt_py_common


1. 子包结构

1
2
3
4
5
6
rqt/
├── rqt/ # meta,聚合依赖
├── rqt_gui/ # ros2 run rqt_gui rqt_gui
├── rqt_gui_py/ # Python 插件 Provider
├── rqt_gui_cpp/ # C++ 插件 Provider
└── rqt_py_common/ # Topic/Node 等 GUI 工具

2. 架构

ros2 run rqt_gui rqt_guirqt_gui.main.Mainqt_gui.main.MainRosPluginProviderRosPyPluginProviderament_index\nplugin.xmlrqt_* 插件包

3. rqt_gui — ROS 入口

3.1 Main 类

1
2
3
4
5
6
7
8
class Main(Base):
def __init__(self, filename=None, settings_filename='rqt_gui'):
qtgui_path = get_package_path('qt_gui')
super(Main, self).__init__(qtgui_path, ...)

def main(self, argv=None, standalone=None, ...):
argv = rclpy.utilities.remove_ros_args(args=argv)
return super(Main, self).main(argv, standalone=standalone, ...)
  • 继承 qt_gui.main.Main
  • 剥离 ROS remapping 参数
  • 设置 rqt 图标与 help 文本
  • _add_plugin_providers() 注册 ROS 专用 Provider

3.2 可执行文件

  • bin/rqt_guirqt_gui.main:main
  • 各插件包 scripts/rqt_graph 等以 standalone='module.Class' 单插件启动

4. rqt_gui_py — Python 插件

4.1 plugin.xml

1
2
3
<class name="RosPyPluginProvider"
type="rqt_gui_py.ros_py_plugin_provider.RosPyPluginProvider"
base_class_type="rqt_gui_py::PluginProvider">

4.2 插件基类

Python 插件继承 rqt_gui_py.plugin.Plugin(或旧名 qt_gui_py.plugin.Plugin),实现:

  • startup_plugin() / shutdown_plugin()
  • 可选 _widget 返回 Qt 控件

4.3 ros2_plugin_context

ros2_plugin_context.py 提供 rclpy Node、namespace 等 ROS 2 上下文给插件。


5. rqt_gui_cpp — C++ 插件

  • RosCppPluginProvider 加载 rqt_gui_cpp::Plugin 子类
  • 适用于高性能 GUI(较少使用)

6. rqt_py_common

共享 Python 模块:

  • 话题列表、消息类型工具
  • rclpy 交互的 helper
  • rqt_topicrqt_plot 等依赖

7. 插件注册约定

每个功能包提供:

  1. plugin.xml — 声明 <class ... base_class_type="rqt_gui_py::Plugin">
  2. setup.pyentry_points + ament index export
  3. resource/ — UI、图标

RecursivePluginProviderament_indexrqt_gui / qt_gui 插件索引中扫描。


8. standalone 模式

1
2
# rqt_graph/scripts/rqt_graph
Main().main(sys.argv, standalone='rqt_graph.ros_graph.RosGraph')

只加载指定插件,不显示空壳主界面。


9. 依赖链

1
2
3
4
5
rqt_gui
├── qt_gui
├── python_qt_binding
├── rclpy
└── rqt_gui_py / rqt_gui_cpp

10. 与 ros2/rviz 区别

工具 用途
rqt qt_gui + 插件 2D GUI、introspection
rviz2 Ogre + rviz_common 3D 可视化

二者独立;interactive_markers 服务 rviz,不服务 rqt。


11. 推荐阅读顺序

  1. rqt_gui/main.py
  2. rqt_gui/ros_plugin_provider.py
  3. rqt_gui_py/ros_py_plugin_provider.py
  4. 任选 rqt_graph/plugin.xml + Plugin 类
  5. rqt_py_common 常用 helper

12. 小结

rqt 目录是 ROS 2 的 qt_gui 启动器 + 插件 Provider:本身不提供业务功能,而是让数十个 rqt_* 插件以统一方式被发现、停靠与持久化布局。

tango_icons_vendor 源码详细分析

tango_icons_vendor 源码详细分析

工作区路径:/home/cp/work2/ros2Learn/ros2_humble/src/ros-visualization/tango_icons_vendor
版本:0.1.1,许可证 BSD

tango_icons_vendorTango 图标主题 的 vendor 包:在非 Linux 平台(Windows/macOS)为 Qt 应用提供与 Linux 桌面一致的 theme 图标,供 rqt/qt_gui 菜单与工具栏使用。


1. 仓库结构

1
2
3
4
tango_icons_vendor/
├── resource/ # Tango 图标 PNG/SVG 资源
├── CMakeLists.txt # install 资源到 share
└── package.xml

2. 作用

rqt 插件 plugin.xml 中常见:

1
<icon type="theme">preferences-system-network</icon>

Linux 上由系统主题解析;Windows/macOS 依赖本包安装的 Tango 图标路径,qt_gui.icon_loader 加载。


3. 依赖关系

1
qt_gui (icon_loader.py) → tango_icons_vendor (share 路径)

无编译代码,纯 资源 vendor 包。


4. 小结

体量最小,却是跨平台 rqt 图标显示 的必要依赖;Linux 开发机常感知不明显,Windows 缺此包时插件菜单图标会缺失。

class_loader 源码详细分析

class_loader 源码详细分析

工作区路径:/home/cp/work2/ros2Learn/ros2_humble/src/ros/class_loader
版本:2.2.0package.xml),语言 C++14,许可证 BSD,Quality Level 1

class_loaderROS 无关的运行时插件加载库:通过 dlopen/LoadLibrary 打开 .so/.dll,在库加载时自动注册工厂(MetaObject),运行时按类名 new 出插件实例,无需在编译期链接插件头文件。ROS 2 中 pluginlib 在其之上封装 XML 清单与包名解析;rclcpp_components 则直接用 CLASS_LOADER_REGISTER_CLASS 实现组件节点动态加载。


1. 总体架构

应用 / pluginlib / ComponentManagerclass_loader::impl 全局状态插件 .soOS / rcpputilsloadLibrary触发静态初始化createInstanceunloadLibraryClassLoader / MultiLibraryClassLoaderBaseToFactoryMapMap<br/>基类 → 类名 → MetaObjectLoadedLibraryVector<br/>已加载 .so 句柄MetaObjectGraveyard<br/>卸载暂存工厂CLASS_LOADER_REGISTER_CLASS<br/>静态 ProxyExec 构造MetaObject<Derived, Base>rcpputils::SharedLibrary<br/>dlopen/dlclose
层次 文件 职责
用户 API class_loader.hpp 单库 ClassLoader
多库 API multi_library_class_loader.hpp 管理多个 ClassLoader
核心实现 class_loader_core.hpp/.cpp 全局工厂表、load/unload
工厂 meta_object.hpp/.cpp MetaObject<C,B> 模板
注册宏 register_macro.hpp 静态初始化注册
底层加载 rcpputils::SharedLibrary 跨平台动态库

源码规模很小:4 个 .cpp、约 6 个头文件、39 个仓库文件。


2. 核心概念

2.1 MetaObject(工厂)

每个可实例化的插件类对应一个 MetaObject<Derived, Base>

1
2
3
4
B * create() const
{
return new C;
}
  • AbstractMetaObjectBase:非模板基类,存类名、基类名、关联库路径、所属 ClassLoader 列表
  • 基类索引键为 typeid(Base).name()(Linux 上常为 mangled 名,Windows 上常为字面类名)
  • 用户可见类名为 未 mangled 的字符串(如 "Dog"

2.2 全局工厂表

1
2
typedef std::map<ClassName, impl::AbstractMetaObjectBase *> FactoryMap;
typedef std::map<BaseClassName, FactoryMap> BaseToFactoryMapMap;

结构:typeid(Base).name(){ "Dog" → MetaObject*, "Cat" → MetaObject*, ... }

所有 ClassLoader 实例共享这一全局表,通过 MetaObject 的 owner ClassLoader 列表区分“谁能创建谁”。

2.3 ClassLoader 与库的作用域

  • isLibraryLoadedByAnybody():物理上 .so 是否已在进程内 dlopen
  • isLibraryLoaded()(对本 ClassLoader):库已加载 该 loader 拥有对应 MetaObject 的创建权限
  • 多个 ClassLoader 打开同一库时,第二个 loader 不会重复 dlopen,只会 addOwningClassLoader

3. 插件注册机制

3.1 CLASS_LOADER_REGISTER_CLASS 宏

1
2
3
4
5
6
7
8
9
10
11
12
#define CLASS_LOADER_REGISTER_CLASS_INTERNAL_WITH_MESSAGE(Derived, Base, UniqueID, Message) \
namespace \
{ \
struct ProxyExec ## UniqueID \
{ \
ProxyExec ## UniqueID() \
{ \
class_loader::impl::registerPlugin<_derived, _base>(#Derived, #Base); \
} \
}; \
static ProxyExec ## UniqueID g_register_plugin_ ## UniqueID; \
} // namespace

机制:

  1. 插件 .cpp 末尾写 CLASS_LOADER_REGISTER_CLASS(Dog, Base)
  2. 生成 静态全局对象 g_register_plugin_N
  3. dlopen 加载 .so 时,静态对象构造 → 调用 registerPlugin
  4. registerPlugin 创建 MetaObject 并插入全局 FactoryMap

测试插件示例(test/plugins1.cpp):

1
2
3
4
5
CLASS_LOADER_REGISTER_CLASS(Dog, Base)
CLASS_LOADER_REGISTER_CLASS(Cat, Base)
CLASS_LOADER_REGISTER_CLASS(Duck, Base)
CLASS_LOADER_REGISTER_CLASS(Cow, Base)
CLASS_LOADER_REGISTER_CLASS(Sheep, Base)

3.2 loadLibrary 时的上下文

loadLibrarydlopen 前设置:

1
2
setCurrentlyActiveClassLoader(loader);
setCurrentlyLoadingLibraryName(library_path);

这样 registerPlugin 能把 MetaObject 绑定到正确的 ClassLoader库路径


4. 实例创建与生命周期

4.1 三种创建方式

方法 返回类型 析构 适用
createInstance<Base>() shared_ptr<Base> 自定义 deleter → onPluginDeletion 推荐
createUniqueInstance<Base>() unique_ptr<Base, Deleter> 同上 独占所有权
createUnmanagedInstance<Base>() Base* 调用方负责 不推荐;会禁用安全 unload

createRawInstance 核心逻辑:

1
2
3
4
5
6
7
8
if (!isLibraryLoaded()) {
loadLibrary();
}
Base * obj = class_loader::impl::createInstance<Base>(derived_class_name, this);
// ...
if (managed) {
++plugin_ref_count_;
}

4.2 On-Demand Load/Unload

构造参数 ondemand_load_unload(默认 false):

  • false:构造 ClassLoader 时立即 loadLibrary()
  • true:首次 createInstance 时加载;最后一个 managed 实例销毁且 plugin_ref_count_==0 时尝试 unloadLibraryInternal

onPluginDeletion 在 shared_ptr/unique_ptr 释放时:

  1. delete obj
  2. --plugin_ref_count_
  3. 若 on-demand 且无 unmanaged 实例 → 卸载库

4.3 引用计数

计数器 含义
load_ref_count_ loadLibrary() / unloadLibrary() 调用次数差
plugin_ref_count_ 当前存活的 managed 插件实例数

卸载前若 plugin_ref_count_ > 0警告且拒绝 unload


5. loadLibrary / unloadLibrary 详细流程

5.1 loadLibrary(class_loader_core.cpp

1
2
3
4
5
6
7
8
9
10
loadLibrary(path, loader)
├─ 若 isLibraryLoadedByAnybody(path)
│ └─ addClassLoaderOwnerForAllExistingMetaObjectsForLibrary
├─ 否则
│ ├─ setCurrentlyActiveClassLoader(loader)
│ ├─ SharedLibrary(path) → dlopen,触发静态注册
│ └─ 加入 LoadedLibraryVector
└─ Graveyard 处理
├─ 无新 MetaObject → revivePreviouslyCreateMetaobjectsFromGraveyard
└─ 有新 MetaObject → purgeGraveyardOfMetaobjects(delete=true)

Graveyard(墓地):卸载库时不真正 delete MetaObject,而是移入 graveyard。原因是 RTLD_GLOBAL 下符号可能未真正卸载,再次 dlopen 时静态注册可能不会重跑,需要从 graveyard 复活工厂。

5.2 unloadLibrary

1
2
3
4
5
6
7
unloadLibrary(path, loader)
├─ 若 hasANonPurePluginLibraryBeenOpened() → 拒绝卸载(全局锁死)
├─ destroyMetaObjectsForLibrary → 从 FactoryMap 移除,移入 graveyard
├─ 若无任何 MetaObject 仍关联该库
│ ├─ SharedLibrary::unload_library()
│ └─ 从 LoadedLibraryVector 移除
└─ 否则保留 .so(其他 ClassLoader 仍在用)

6. MultiLibraryClassLoader

路径:multi_library_class_loader.hpp/.cpp

  • 内部维护 map<library_path, ClassLoader*>
  • loadLibrary(path) 为每个 path 创建一个 ClassLoader
  • createInstance<Base>(class_name) 遍历所有 loader,找第一个 isClassAvailable
  • createInstance(class_name, library_path) 指定库,避免同名类冲突

pluginlibClassLoader<T> 底层即 MultiLibraryClassLoader lowlevel_class_loader_(false)(非 on-demand)。


7. 依赖关系

1
2
3
4
class_loader
├── rcpputils::SharedLibrary (dlopen 封装)
├── console_bridge (日志 CONSOLE_BRIDGE_logDebug)
└── ament_cmake / ament_cmake_ros

systemLibraryFormat() 委托 rcpputils::get_platform_library_name() 生成 libfoo.so 等平台名。


8. 符号可见性与“纯插件库”约束

8.1 hide_library_symbols

cmake/class_loader_hide_library_symbols.cmakelinker version script 隐藏插件库中除注册所需符号外的全部符号,减少与主程序符号冲突。

8.2 非纯插件库问题

若插件库被可执行文件直接链接(而非仅 dlopen),会在 main 之前触发 dlopen,此时:

  • getCurrentlyActiveClassLoader()nullptr
  • 设置 hasANonPurePluginLibraryBeenOpened(true)
  • 之后任何库都无法安全 unload

registerPlugin 中会打印 SEVERE WARNING 提示将插件隔离到独立 .so


9. 与 ROS 2 的集成

9.1 分层关系

1
2
3
4
5
6
7
应用 (rviz / nav2 / ...)

pluginlib::ClassLoader<T> ← plugin.xml + ament_index 解析

class_loader::MultiLibraryClassLoader

class_loader::ClassLoader + dlopen

PLUGINLIB_EXPORT_CLASS 就是 CLASS_LOADER_REGISTER_CLASS 的别名:

1
2
#define PLUGINLIB_EXPORT_CLASS(class_type, base_class_type) \
CLASS_LOADER_REGISTER_CLASS(class_type, base_class_type)

9.2 rclcpp_components(组件节点)

不经过 pluginlib XML,直接注册:

1
2
3
4
#define RCLCPP_COMPONENTS_REGISTER_NODE(NodeClass) \
CLASS_LOADER_REGISTER_CLASS( \
rclcpp_components::NodeFactoryTemplate<NodeClass>, \
rclcpp_components::NodeFactory)

ComponentManagerclass_loader::ClassLoader 加载 libxxx_component.so,按类名创建 NodeFactory 再构造节点。

9.3 典型使用场景

场景 基类 注册宏
rviz 插件 rviz_common::Display / Tool PLUGINLIB_EXPORT_CLASS
costmap 层 nav2_costmap_2d::Layer PLUGINLIB_EXPORT_CLASS
组件节点 rclcpp_components::NodeFactory RCLCPP_COMPONENTS_REGISTER_NODE
非 ROS 插件 自定义 Base CLASS_LOADER_REGISTER_CLASS

10. 异常类型

异常 触发条件
LibraryLoadException dlopen 失败
LibraryUnloadException dlclose 失败或库未注册
CreateClassException 类名不存在或 factory 不属当前 loader
NoClassLoaderExistsException MultiLibrary 未 load 指定库

11. 目录与文件对照

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class_loader/
├── include/class_loader/
│ ├── class_loader.hpp # 用户主 API
│ ├── multi_library_class_loader.hpp
│ ├── class_loader_core.hpp # impl 命名空间 + 模板
│ ├── meta_object.hpp # MetaObject 工厂
│ ├── register_macro.hpp # CLASS_LOADER_REGISTER_CLASS
│ ├── exceptions.hpp
│ └── visibility_control.hpp # Windows DLL 导出
├── src/
│ ├── class_loader.cpp # ClassLoader 方法
│ ├── class_loader_core.cpp # 全局状态 + load/unload
│ ├── meta_object.cpp # MetaObject 基类实现
│ └── multi_library_class_loader.cpp
├── cmake/class_loader_hide_library_symbols.cmake
├── class_loader-extras.cmake # 供插件 CMake 调用 hide symbols
└── test/ # Dog/Cat 等插件单元测试

12. 设计特点与注意事项

特点 说明
静态注册 依赖 C++ 动态库加载时的静态对象构造,无需 dlsym 查符号
全局工厂表 进程级单例;多 loader 共享,靠 owner 列表隔离
Graveyard 应对 RTLD_GLOBAL 下重复 load/unload 时工厂不重建
typeid 键 跨平台基类名可能 mangled,模板参数 Base 必须一致
managed 实例 shared_ptr deleter 耦合引用计数与 on-demand unload
unmanaged 陷阱 一旦创建,进程内 on-demand unload 可能永久失效
线程安全 recursive_mutex 保护全局表与库向量

13. 推荐阅读顺序

  1. 注册与实例化register_macro.hpptest/plugins1.cppclass_loader.hppcreateInstance
  2. 全局状态class_loader_core.hppregisterPlugin / createInstance
  3. load/unloadclass_loader_core.cpploadLibrary / unloadLibrary / graveyard
  4. ROS 封装pluginlib/class_loader.hpp + class_list_macros.hpp
  5. 组件节点rclcpp_components/register_node_macro.hpp + component_manager.cpp
  6. 调试impl::printDebugInfoToScreen() 打印已加载库与 MetaObject

14. 与 CycloneDDS / iceoryx 的对比

维度 class_loader CycloneDDS / iceoryx
职责 C++ 类插件动态实例化 进程间通信
加载对象 .so 中的 C++ 类 无(或 SHM/RTPS 库)
ROS 2 角色 rviz/nav2/组件节点基础设施 中间件传输

如果你希望,我可以把本文写入 ros2doc/ 下对应 markdown,或继续追踪 pluginlib 如何从 plugin.xml 解析出类名并调用 MultiLibraryClassLoader::loadLibrary 的完整链路。

kdl_parser 源码详细分析

kdl_parser 源码详细分析

工作区路径:/home/cp/work2/ros2Learn/ros2_humble/src/ros/kdl_parser
版本:2.6.4,许可证 BSD。仓库含两个包:kdl_parser(C++)与 kdl_parser_py(Python)。

kdl_parser 的职责很单一:把 URDF 机器人模型 转成 Orocos KDL 的 KDL::Tree,供正/逆运动学、动力学求解使用。它不做 URDF 解析本身(交给 urdf / urdf_parser_py),也不发布 TF(那是 robot_state_publisher 的事)。


1. 总体认识

1.1 在 ROS 2 栈中的位置

URDF XML / robot_descriptionurdf::Model<br/>urdf_parser_pykdl_parserKDL::Tree / PyKDL.Treerobot_state_publisherKDL 求解器<br/>Fk/IK/Id 等
语言 依赖 主要消费者(本工作区)
kdl_parser C++14 urdf, orocos_kdl, rcutils robot_state_publisher
kdl_parser_py Python urdfdom_py, python_orocos_kdl 测试/脚本(ROS 2 中较少直接使用)

2. 仓库结构

1
2
3
4
5
6
7
8
9
10
11
12
kdl_parser/
├── kdl_parser/ # C++ 库(ament_cmake)
│ ├── include/kdl_parser/
│ │ ├── kdl_parser.hpp # 公开 API(3 个函数)
│ │ └── visibility_control.hpp
│ ├── src/
│ │ ├── kdl_parser.cpp # 核心转换逻辑(~210 行)
│ │ └── check_kdl_parser.cpp # 命令行调试工具
│ └── test/ # gtest + PR2/r2d2 URDF
├── kdl_parser_py/ # Python 包
│ └── kdl_parser_py/urdf.py # 与 C++ 平行的转换逻辑
└── README.md

体量很小:C++ 核心仅 一个源文件,公开 API 3 个函数


3. C++ API

1
2
3
4
5
6
7
8
KDL_PARSER_PUBLIC
bool treeFromFile(const std::string & file, KDL::Tree & tree);

KDL_PARSER_PUBLIC
bool treeFromString(const std::string & xml, KDL::Tree & tree);

KDL_PARSER_PUBLIC
bool treeFromUrdfModel(const urdf::ModelInterface & robot_model, KDL::Tree & tree);
函数 输入 流程
treeFromFile URDF 文件路径 读文件 → treeFromString
treeFromString URDF XML 字符串 urdf::Model::initStringtreeFromUrdfModel
treeFromUrdfModel 已解析的 URDF 模型 递归构建 KDL::Tree

4. 核心转换逻辑(C++)

4.1 数据流

1
2
3
4
5
6
7
8
treeFromUrdfModel
├─ tree = KDL::Tree(root_link_name) # 仅设根名,根 link 不建 segment
├─ 警告:根 link 有 inertia(KDL 不支持)
└─ 对每个 root 的子 link 递归 addChildrenToTree
├─ toKdl(inertial) → RigidBodyInertia
├─ toKdl(parent_joint) → KDL::Joint
├─ Segment(link_name, joint, origin, inertia)
└─ tree.addSegment(segment, parent_link_name)

4.2 URDF → KDL 类型映射

位姿/几何

URDF KDL 实现
Vector3 KDL::Vector 直接拷贝 x,y,z
Rotation (四元数) KDL::Rotation Rotation::Quaternion(x,y,z,w)
Pose KDL::Frame Frame(R, p)

关节toKdl(urdf::JointSharedPtr)

URDF Joint 类型 KDL Joint 类型 说明
FIXED Joint::None 固定关节
REVOLUTE Joint::RotAxis 旋转轴 = F_parent_jnt.M * axis
CONTINUOUS Joint::RotAxis 同 revolute,无限位
PRISMATIC Joint::TransAxis 平移轴
其他(floating/planar 等) Joint::None 警告后当 fixed 处理

关节原点与轴的处理:

1
2
3
4
5
6
7
8
KDL::Joint toKdl(urdf::JointSharedPtr jnt)
{
KDL::Frame F_parent_jnt = toKdl(jnt->parent_to_joint_origin_transform);
// ...
case urdf::Joint::REVOLUTE: {
KDL::Vector axis = toKdl(jnt->axis);
return KDL::Joint(jnt->name, F_parent_jnt.p, F_parent_jnt.M * axis, KDL::Joint::RotAxis);
}

即:关节锚点在父 link 坐标系中的位置为 F_parent_jnt.p,旋转/平移轴在父 link 系下为 F_parent_jnt.M * axis(与 KDL Segment 约定一致)。

惯性toKdl(urdf::InertialSharedPtr)

这是转换中最 subtle 的部分:

1
2
3
4
5
6
7
8
// URDF 惯性矩阵在 inertial 参考系;KDL 要求在 link 参考系
KDL::RotationalInertia urdf_inertia = ...;
// 用 RigidBodyInertia 运算符 workaround 做旋转
KDL::RigidBodyInertia kdl_inertia_wrt_com_workaround =
origin.M * KDL::RigidBodyInertia(0, KDL::Vector::Zero(), urdf_inertia);
KDL::RotationalInertia kdl_inertia_wrt_com =
kdl_inertia_wrt_com_workaround.getRotationalInertia();
return KDL::RigidBodyInertia(kdl_mass, kdl_com, kdl_inertia_wrt_com);
  • 质量、质心:URDF 与 KDL 都在 link 坐标系 下表达 COM
  • 惯性张量:URDF 在 inertial 原点坐标系;KDL 需要 相对 COM 且在 link 系 的张量
  • 通过 origin.M 旋转张量,再提取 getRotationalInertia()

test_inertia_rpy.cpp递归牛顿-欧拉逆动力学 对比两种 URDF 惯性描述,验证转换正确性。

4.3 KDL Tree 与 URDF 的对应关系

URDF 概念 KDL 概念
link(除根外) Segment(含 name、joint、tip frame、inertia)
joint 挂在 子 segment 上的 KDL::Joint
根 link 仅作为 Tree 根名;不生成带惯性的 segment
固定关节 Joint::None

测试 URDF test_robot.urdf 使用 dummy_link 技巧:根 link 无 inertia,真实 base 通过 fixed joint 挂在 dummy 下,以满足 KDL 限制:

1
2
3
4
5
6
<link name="dummy_link"/>
...
<joint name="dummy_to_base" type="fixed">
<parent link="dummy_link"/>
<child link="base_link"/>
</joint>

5. Python 包(kdl_parser_py)

路径:kdl_parser_py/kdl_parser_py/urdf.py

与 C++ 逻辑平行,差异如下:

方面 C++ Python
URDF 解析 urdf::Model urdf_parser_py.urdf.URDF
KDL 绑定 orocos_kdl (C++) PyKDL
返回值 bool (ok, tree) 元组
位姿 四元数 Rotation RPY + XYZpose.rpy, pose.xyz
树遍历 child_links 递归 child_map / parent_map / link_map
treeFromParam 读 ROS 参数服务器(ROS 1 风格,ROS 2 中通常不用)

关节映射(Python 显式处理更多类型):

1
2
3
4
5
6
7
8
9
type_map = {
'fixed': fixed,
'revolute': rotational,
'continuous': rotational,
'prismatic': translational,
'floating': fixed,
'planar': fixed,
'unknown': fixed,
}

注意:Python 版 floating/planar 静默变为 fixed;C++ 版对未知类型会 RCUTILS_LOG_WARN


6. 构建与依赖

kdl_parser(C++)

1
2
3
4
5
6
7
add_library(${PROJECT_NAME} src/kdl_parser.cpp)
target_link_libraries(${PROJECT_NAME} PUBLIC
orocos-kdl
urdfdom_headers::urdfdom_headers)
target_link_libraries(${PROJECT_NAME} PRIVATE
rcutils::rcutils
urdf::urdf)
  • orocos_kdl_vendor:ROS 2 将 Orocos KDL 以 vendor 形式打进工作区
  • urdf:ROS 2 的 URDF C++ 解析库(基于 urdfdom)
  • 日志:RCUTILS_LOG_*_NAMED("kdl_parser", ...)

7. ROS 2 中的主要消费者:robot_state_publisher

1
2
3
4
5
6
7
8
9
10
11
KDL::Tree RobotStatePublisher::parseURDF(const std::string & urdf_xml, urdf::Model & model)
{
if (!model.initString(urdf_xml)) {
throw std::runtime_error("Unable to initialize urdf::model from robot description");
}
KDL::Tree tree;
if (!kdl_parser::treeFromUrdfModel(model, tree)) {
throw std::runtime_error("Failed to extract kdl tree from robot description");
}
return tree;
}

robot_state_publisher 用 KDL Tree 做:

  1. 遍历 segment,区分 fixed可动 关节
  2. 结合 /joint_states正向运动学,发布 TF
  3. mimic 关节 在 RSP 里单独处理(kdl_parser 不解析 mimic)

即:kdl_parser 提供 kinematic tree 结构;RSP 负责运行时状态与 TF。


8. 工具与测试

文件 用途
check_kdl_parser.cpp CLI:解析 URDF 并打印 segment 树
test_kdl_parser.cpp 验证 r2d2 URDF:8 joints、16 segments、惯性数值
test_inertia_rpy.cpp 两种 inertia 描述的动力学等价性
test/*.xml PR2 等复杂模型(部分应解析失败)
kdl_parser_py/test/ Python 侧 rostest

9. 限制与注意事项

限制 说明
根 link 惯性 KDL 根 segment 不支持 inertia;需 dummy fixed link
floating / planar 转为 fixed,多自由度关节不被 KDL Tree 表达
mimic 关节 不在 kdl_parser 处理;由上层(RSP)扩展
闭环机构 URDF 为树;闭环需额外约束(KDL 不直接支持)
语义 vs 几何 material/visual/collision 被忽略,只取 kinematics + inertia
双包一致性 C++ 用四元数、Python 用 RPY,极端姿态下需留意数值差异

10. 与相关包对比

输入 输出 用途
urdf XML urdf::Model 对象图 解析、修改 URDF
kdl_parser URDF 模型 KDL::Tree 运动学/动力学计算
robot_state_publisher URDF + joint_states TF 运行时状态
tf2 几何变换 TF 缓冲 坐标变换查询

11. 推荐阅读顺序

  1. 公开 APIkdl_parser.hpp — 三个入口函数
  2. 转换细节kdl_parser.cpptoKdl 系列 + addChildrenToTree
  3. 测试 URDFtest/test_robot.urdf — dummy link 模式
  4. 惯性验证test/test_inertia_rpy.cpp
  5. ROS 2 集成robot_state_publisher/src/robot_state_publisher.cppparseURDF / addChildren
  6. Python 对照kdl_parser_py/urdf.py

12. 设计特点小结

特点 说明
薄适配层 几乎只做 URDF→KDL 字段映射,无独立状态机
单文件核心 维护成本低,行为清晰
递归建树 与 URDF 树结构一一对应
惯性变换 正确处理 inertial frame → link frame
双语言实现 C++ 为主路径;Python 供脚本/原型
ROS 解耦 库本身不依赖 rclcpp(除日志用 rcutils)

如果你希望,我可以把本文写入 ros2doc/ros/kdl_parser 源码详细分析.md,或继续分析 robot_state_publisher 如何用 KDL Tree + joint_states 计算 TF 的完整链路。

pluginlib 源码详细分析

pluginlib 源码详细分析

工作区路径:/home/cp/work2/ros2Learn/ros2_humble/src/ros/pluginlib
版本:5.1.4pluginlib 包),许可证 BSD

pluginlibclass_loader 之上封装 ROS/ament 的插件发现与清单机制:插件提供方在构建时注册 XML 描述文件,运行时用 pluginlib::ClassLoader<T> 按 lookup 名加载 .so 并实例化插件类。ROS 2 中 rviznav2image_transport 等大量扩展点都依赖它。


1. 仓库结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pluginlib/
├── pluginlib/ # C++ 核心(header-only INTERFACE 库)
│ ├── include/pluginlib/
│ │ ├── class_loader.hpp # 模板 ClassLoader API
│ │ ├── class_loader_imp.hpp # 实现(~850 行,被 include)
│ │ ├── class_loader_base.hpp # 非模板管理接口
│ │ ├── class_desc.hpp # 插件元数据
│ │ ├── class_list_macros.hpp # PLUGINLIB_EXPORT_CLASS
│ │ ├── exceptions.hpp
│ │ └── impl/split.hpp
│ ├── src/list_plugins.cpp # CLI 列出插件
│ ├── cmake/ # 构建/注册宏
│ └── test/
└── ros2plugin/ # ros2 CLI 插件(Python)
└── ros2plugin/verb/list.py # ros2 plugin list

特点pluginlib 本身是 INTERFACE 库(无 .cpp 编译进库),实现全在头文件 class_loader_imp.hpp 中。


2. 与 class_loader 的分工

插件包 my_plugins构建时运行时 宿主 rviz/nav2解析 XML + ament_indexplugins.xmllibmy_plugins.soPLUGINLIB_EXPORT_CLASSpluginlib_export_plugin_description_fileament_index 资源pluginlib::ClassLoaderclass_loader::MultiLibraryClassLoaderdlopen + MetaObject
层次 职责
pluginlib XML 清单、ament 索引、lookup 名→C++ 类名、库路径搜索
class_loader dlopen、静态注册、MetaObject 工厂、createInstance

PLUGINLIB_EXPORT_CLASS 就是 CLASS_LOADER_REGISTER_CLASS 的别名:

1
2
#define PLUGINLIB_EXPORT_CLASS(class_type, base_class_type) \
CLASS_LOADER_REGISTER_CLASS(class_type, base_class_type)

3. 插件注册(构建时)

3.1 CMake 宏

插件包在 CMakeLists.txt 中调用:

1
pluginlib_export_plugin_description_file(rviz "plugin_description.xml")

宏逻辑(cmake/pluginlib_export_plugin_description_file.cmake):

  1. 安装 XML 到 share/<package>/...
  2. 累积 ament index 资源内容(相对路径 + 换行)
  3. ament_package() 时由 pluginlib_package_hook.cmake 写入 index

资源名约定:

1
<plugin_category>__pluginlib__plugin

例如 rviz 的 Display 插件:rviz__pluginlib__plugin
资源内容为多行相对路径,如 share/rviz_default_plugins/plugins.xml

3.2 插件 XML 格式

1
2
3
4
5
6
<library path="test_plugins">
<class name="test_pluginlib/foo" type="test_plugins::Foo" base_class_type="test_base::Fubar">
<description>This is a foo plugin.</description>
</class>
...
</library>
属性/标签 含义
<library path="..."> 库名(非完整路径),如 test_pluginslibtest_plugins.so
class/@name lookup_name(对外插件 ID,可含 /
class/@type C++ 完整类名(传给 class_loader
class/@base_class_type 基类字符串,须与 ClassLoader 构造参数一致
<description> 人类可读描述

也支持根标签 <class_libraries> 包裹多个 <library>

3.3 插件 .so 内注册

1
2
PLUGINLIB_EXPORT_CLASS(test_plugins::Foo, test_base::Fubar)
PLUGINLIB_EXPORT_CLASS(test_plugins::Bar, test_base::Fubar)

type 属性必须与 PLUGINLIB_EXPORT_CLASS 的第一个参数一致。


4. ClassLoader 运行时流程

4.1 构造

1
2
3
4
5
6
7
8
9
10
11
12
13
14
ClassLoader<T>::ClassLoader(
std::string package,
std::string base_class,
std::string attrib_name,
std::vector<std::string> plugin_xml_paths)
: ...
lowlevel_class_loader_(false) // 非 on-demand,库由 pluginlib 显式 load/unload
{
ament_index_cpp::get_package_prefix(package_); // 校验 package 存在
if (plugin_xml_paths_.empty()) {
plugin_xml_paths_ = getPluginXmlPaths(package_, attrib_name_);
}
classes_available_ = determineAvailableClasses(plugin_xml_paths_);
}

参数说明:

  • package:插件类别(plugin_category),不是插件所在包名。如 rviz 用 "rviz_common""rviz",决定查哪个 ament index 资源。
  • base_class:基类类型字符串,用于 XML 过滤。
  • plugin_xml_paths:可选,手动指定 XML 路径;默认从 index 自动发现。

4.2 发现 plugin XML(ament_index)

1
2
3
std::string resource_name = package + "__pluginlib__" + attrib_name;
auto plugin_packages_with_prefixes = ament_index_cpp::get_resources(resource_name);
// 每个导出插件的包:prefix + "/" + 相对路径

遍历所有向该 category 注册了插件的包,收集全部 XML 绝对路径。

4.3 解析 XML → ClassDesc

processSingleXMLPluginFiletinyxml2 解析,仅当 base_class_type == base_class_ 时插入 classes_available_

1
2
3
classes_available.insert(std::pair<std::string, ClassDesc>(lookup_name,
ClassDesc(lookup_name, derived_class, base_class_type, package_name, description_str,
library_path, xml_file)));

ClassDesc 字段见 class_desc.hpplookup_namederived_class_library_name_resolved_library_path_(加载后填充)等。

4.4 加载库并实例化

1
2
3
4
5
6
createUniqueInstance(lookup_name)
→ loadLibraryForClass(lookup_name)
→ getClassLibraryPath(lookup_name) # 搜索 lib/ lib64/ bin/ ...
→ lowlevel_class_loader_.loadLibrary(path)
→ lowlevel_class_loader_.createUniqueInstance<T>(getClassType(lookup_name))
# getClassType 返回 XML 中的 type,即 C++ 类名

库路径搜索(getAllLibraryPathsToTry)在导出插件的包 prefix 下尝试:

  • lib/lib64/bin/(Windows DLL)
  • 带/不带 lib 前缀、debug 后缀等多种组合

4.5 实例化 API

方法 所有权 说明
createUniqueInstance unique_ptr + 自定义 deleter 推荐
createUnmanagedInstance 原始指针 调用方负责释放与 unload
createClassInstance 原始指针 已 deprecated
createSharedInstance 声明为 shared_ptr 实现直接 return createUniqueInstance(类型不匹配,实际不宜使用)

lowlevel_class_loader_(false) 表示不用 class_loader 的 on-demand 模式;库的 load/unload 由 loadLibraryForClass / unloadLibraryForClass 管理。


5. ClassLoaderBase

非模板基类 ClassLoaderBase 暴露所有管理类接口(列举、查询、load/unload),不含 createInstance。便于写不依赖具体插件类型的管理代码。


6. 工具与 CLI

6.1 list_plugins(C++)

1
2
3
4
pluginlib::ClassLoader<void> cl(argv[1], argv[2]);
for (const auto & declared : cl.getDeclaredClasses()) {
std::cout << declared << std::endl;
}

用法:list_plugins <package/category> <base_class_type>
ClassLoader<void> 仅用于调用 getDeclaredClasses(),不能 createInstance

6.2 ros2 plugin list(Python)

ros2plugin/verb/list.py 直接读 ament index + 解析 XML,不实例化插件。支持:

  • ros2 plugin list --packages
  • ros2 plugin list --package <name>

7. ROS 2 典型用法:rviz

1
2
3
4
5
PluginlibFactory(const QString & package, const QString & base_class_type)
{
class_loader_ = new pluginlib::ClassLoader<Type>(
package.toStdString(), base_class_type.toStdString());
}

rviz 插件包侧:

  1. CMakeLists.txtpluginlib_export_plugin_description_file(...)
  2. 各 Display/Tool .cpp 末尾:PLUGINLIB_EXPORT_CLASS(..., rviz_common::Display)
  3. plugins.xml 声明 name/type/base_class_type/library path

用户从 UI 选择插件 → PluginlibFactory::makecreateInstance(lookup_name)


8. 依赖关系

1
2
3
4
5
6
pluginlib (INTERFACE)
├── class_loader
├── ament_index_cpp # 发现 plugin XML
├── tinyxml2 # 解析 XML
├── rcpputils # 文件系统、库名
└── rcutils # 日志

9. 测试基础设施

cmake/pluginlib_enable_plugin_testing.cmake 在测试中模拟完整安装布局:

  • 临时 prefix + AMENT_PREFIX_PATH
  • 注册 mock 的 test_pluginlib__pluginlib__plugin 资源
  • 构建 test_plugins 共享库

测试覆盖:utest、unique_ptr_test、无效 XML、库缺失等。


10. 双名体系(易混淆点)

名称 来源 用途
lookup_name XML class/@name API 参数,如 rviz_default_plugins/Grid
type / derived_class XML class/@type 传给 class_loader 的 C++ 类名
base_class_type XML + 构造参数 过滤插件是否属于当前 ClassLoader
plugin_category (package 参数) CMake pluginlib_export_plugin_description_file 第一参数 ament index 资源键

lookup_name 与 C++ 类名可以不同;getClassType(lookup_name) 做映射。


11. 异常类型

异常 场景
ClassLoaderException 包不存在等
InvalidXMLException XML 格式错误
LibraryLoadException 找不到 .so 或 dlopen 失败
CreateClassException 工厂创建失败(类名/XML/宏不一致)
LibraryUnloadException 卸载失败

12. 与 rclcpp_components 的区别

pluginlib rclcpp_components
清单 plugin XML + ament index 无 XML,直接 CLASS_LOADER_REGISTER_CLASS
发现 按 category 扫描 ComponentManager 按库路径加载
典型用途 rviz、nav2、costmap 插件 同进程多节点组件
底层 class_loader class_loader

13. 推荐阅读顺序

  1. 插件作者视角class_list_macros.hpp → 测试 test_plugins.xml + test_plugins.cpppluginlib_export_plugin_description_file.cmake
  2. 运行时class_loader.hpp API → class_loader_imp.hppgetPluginXmlPathsprocessSingleXMLPluginFileloadLibraryForClass
  3. 集成示例rviz_common/factory/pluginlib_factory.hpp
  4. 底层机制:[class_loader 分析](ros2doc/ros/class_loader 源码详细分析.md)(若已保存)
  5. CLIlist_plugins.cppros2plugin/verb/list.py

14. 设计特点小结

特点 说明
Header-only 实现 模板实现在 class_loader_imp.hpp
ament index 驱动 跨包发现插件,无需硬编码路径
lookup 名解耦 XML name 与 C++ type 可不同
多库管理 底层 MultiLibraryClassLoader
库路径启发式搜索 兼容 lib/lib64/Windows/debug 等
显式 load/unload 与 class_loader on-demand 分离,便于多插件共享一库

如果你希望,我可以把本文写入 ros2doc/ros/pluginlib 源码详细分析.md,或继续追踪 nav2 / costmap_2d 中 pluginlib 的完整加载链路(从 plugin.xml 到 costmap layer 实例化)。

resource_retriever 源码详细分析

resource_retriever 源码详细分析

工作区路径:/home/cp/work2/ros2Learn/ros2_humble/src/ros/resource_retriever
版本:3.1.3resource_retriever 包),许可证 BSD

resource_retriever 是一个极小的资源加载库:把多种 URI 协议(package://file://http://ftp:// 等)统一解析,并将文件内容读入内存。最初为 rviz 加载 mesh/纹理 设计,现仍是 ROS 2 可视化栈中 package:// 的底层实现之一。


1. 仓库结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
resource_retriever/
├── resource_retriever/ # 主包(C++ 库 + Python 模块)
│ ├── include/resource_retriever/
│ │ ├── retriever.hpp # 公共 API
│ │ └── visibility_control.hpp # 符号导出宏
│ ├── src/
│ │ ├── retriever.cpp # C++ 实现(~145 行)
│ │ └── resource_retriever/
│ │ └── __init__.py # Python API
│ ├── test/
│ │ ├── test.cpp # gtest
│ │ ├── test.py # pytest
│ │ └── test.txt # 测试用 package 资源
│ └── CMakeLists.txt
└── libcurl_vendor/ # libcurl 依赖封装(vendor 或系统库)
├── CMakeLists.txt
├── libcurl_vendor-extras.cmake.in
└── env_hook/ # LD_LIBRARY_PATH / PATH 钩子

特点:核心逻辑不到 200 行;C++ 与 Python 两套 API 语义一致,但实现独立。


2. 架构总览

URI 输入resource_retriever主要消费者转为 file://package://pkg/pathfile:///pathhttp://...ftp://...package:// 解析ament_index_cpplibcurl easy handleMemoryResourcerviz mesh/纹理/图标visualization_msgs Marker
组件 职责
C++ Retriever package:// 解析 + libcurl 下载/读文件
Python get() 同样解析 package://,用 urllib 读取
libcurl_vendor 系统无 curl 时从源码构建 7.81.0 并安装到 opt/libcurl_vendor
ament_index package://foo/... 映射到 share/foo/... 绝对路径

3. 公共 API

3.1 C++:RetrieverMemoryResource

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
struct MemoryResource
{
MemoryResource()
: size(0)
{
}

std::shared_ptr<uint8_t> data;
size_t size;
};

class RESOURCE_RETRIEVER_PUBLIC Retriever
{
public:
Retriever();
~Retriever();
MemoryResource get(const std::string & url);
private:
Retriever(const Retriever & ret) = delete;
CURL * curl_handle_;
};
类型/方法 说明
MemoryResource shared_ptr<uint8_t> + size,二进制缓冲区
Retriever::get(url) 同步获取资源,失败抛 resource_retriever::Exception
Exception 继承 std::runtime_error,格式:Error retrieving file [url]: msg

头文件中 CURL 仅前向声明,避免在公共头里 #include <curl/curl.h>

3.2 Python

1
2
3
4
5
6
7
8
9
10
def get_filename(url, use_protocol=True):
# package:// → file:// 或纯路径
...

def get(url):
filename = get_filename(url)
try:
return urlopen(filename).read()
except URLError:
raise Exception('Invalid URL: {}'.format(filename))
函数 返回值 说明
get_filename(url) str 仅做 URI 转换,不读文件
get(url) bytes 读入内存

Windows 上对 file:// 路径做了特殊处理(反斜杠、/ 前缀)。


4. C++ 核心实现:Retriever::get

4.1 libcurl 全局初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class CURLStaticInit
{
public:
CURLStaticInit()
{
CURLcode ret = curl_global_init(CURL_GLOBAL_ALL);
...
}
~CURLStaticInit()
{
if (initialized_) {
curl_global_cleanup();
}
}
};
static CURLStaticInit g_curl_init;

进程内静态对象保证 curl_global_init / curl_global_cleanup 配对。每个 Retriever 实例持有独立的 curl_easy_init() handle,便于复用连接(注释说明多次访问同一 URL 可保持连接)。

4.2 package:// 解析

1
2
3
4
5
6
7
8
9
10
11
12
MemoryResource Retriever::get(const std::string & url)
{
std::string mod_url = url;
if (url.find("package://") == 0) {
mod_url.erase(0, strlen("package://"));
size_t pos = mod_url.find("/");
...
std::string package = mod_url.substr(0, pos);
...
package_path = ament_index_cpp::get_package_share_directory(package);
mod_url = "file://" + package_path + mod_url;
}

解析规则:

1
2
3
package://<package_name>/<relative_path>

file://<share_dir>/<relative_path>

例如:

1
2
package://resource_retriever/test/test.txt
→ file:///opt/ros/humble/share/resource_retriever/test/test.txt

校验点:

  • 必须有 / 分隔包名与路径
  • 包名不能为空(package:///test.xml 会抛异常)
  • 包不存在 → PackageNotFoundError 被捕获并转为 Exception

4.3 libcurl 读入内存

1
2
3
4
5
6
7
8
9
10
11
curl_easy_setopt(curl_handle_, CURLOPT_URL, mod_url.c_str());
curl_easy_setopt(curl_handle_, CURLOPT_WRITEFUNCTION, curlWriteFunc);
...
CURLcode ret = curl_easy_perform(curl_handle_);
if (ret != 0) {
throw Exception(mod_url, error_buffer);
} else if (!buf.v.empty()) {
res.size = buf.v.size();
res.data.reset(new uint8_t[res.size], std::default_delete<uint8_t[]>());
memcpy(res.data.get(), &buf.v[0], res.size);
}

流程:

  1. curlWriteFunc 把数据追加到 std::vector<uint8_t>
  2. 成功后拷贝到 shared_ptr<uint8_t[]>(从 ROS 1 时代 boost::shared_array 迁移而来)
  3. 空文件时 size=0data 为 null

未设置的超时、重定向限制、SSL 校验等 curl 选项均使用 libcurl 默认值。


5. libcurl_vendor 依赖层

resource_retriever 不直接依赖系统 curl,而是通过 libcurl_vendor

1
2
3
4
5
6
7
find_package(CURL QUIET)

if(FORCE_BUILD_VENDOR_PKG OR NOT CURL_FOUND)
build_libcurl() # ExternalProject 下载 curl-7.81.0
ament_environment_hooks(...) # 设置 LD_LIBRARY_PATH / PATH
set(CURL_FOUND FALSE)
endif()

libcurl_vendor-extras.cmake.in 逻辑:

  1. 优先 find_package(CURL)
  2. 失败则用 vendor 构建产物(Linux 走 pkg-config libcurl,Windows 走 CURL::libcurl target)
  3. 导出 libcurl_vendor_LIBRARIESINCLUDE_DIRS 等供下游链接

CMakeLists 中有 TODO:未来可能去掉 vendor,直接使用系统 curl。


6. 支持的 URI 协议

协议 C++ Python 说明
package:// ✅ 先转 file:// ✅ 同左 ROS 包 share 目录资源
file:// ✅ libcurl ✅ urllib 本地文件
http:// / https:// ✅ libcurl ✅ urllib 网络资源
ftp:// ✅ libcurl ✅ urllib 理论支持,测试较少

不支持embedded://(rviz Marker 专用,由 rviz 层处理,不经过 resource_retriever)。


7. ROS 2 典型消费者

7.1 rviz:图标与 pixmap

1
2
3
4
5
6
7
8
9
10
11
12
resource_retriever::MemoryResource getResource(const std::string & resource_path)
{
resource_retriever::Retriever retriever;
...
try {
res = retriever.get(resource_path);
} catch (resource_retriever::Exception & e) {
RVIZ_COMMON_LOG_DEBUG(e.what());
return resource_retriever::MemoryResource();
}
return res;
}

loadPixmap("package://rviz_common/icons/...")QPixmap::loadFromData

7.2 rviz:mesh 加载(Ogre / Assimp / STL)

1
2
3
4
5
6
7
8
Ogre::MeshPtr loadMeshFromResource(const std::string & resource_path)
{
...
auto res = getResource(resource_path);
// .mesh → Ogre MeshSerializer
// .stl → STLLoader
// 其他 → AssimpLoader(dae/obj 等)
}

Assimp 自定义 IO 系统直接调用 Retriever::get,使主 mesh 与相对路径纹理/子资源都能走同一套 URI 解析:

1
2
3
4
5
6
7
8
9
10
Assimp::IOStream * Open(const char * file, const char * mode = "rb") override
{
resource_retriever::MemoryResource res;
try {
res = retriever_.get(file);
} catch (const resource_retriever::Exception & e) {
return nullptr;
}
return new ResourceIOStream(res);
}

ResourceIOStreamMemoryResource 上实现 Assimp 的 Read/Seek/Tell,无需落盘临时文件。

7.3 rviz:URDF 纹理

1
2
3
4
5
6
7
8
void RobotLink::loadMaterialFromTexture(...)
{
std::string filename = visual->material->texture_filename;
resource_retriever::Retriever retriever;
res = retriever.get(filename); // 可为 package:// 或 http://
...
Ogre::TextureManager::getSingleton().loadImage(...);
}

7.4 visualization_msgs/Marker

Marker 消息文档明确引用 resource_retriever:

1
2
3
4
5
# Texture resource is a special URI ... acceptable to resource retriever
string texture_resource
...
# mesh_resource uses resource retriever to load a mesh.
string mesh_resource

MESH_RESOURCE 类型 marker 的 mesh_resource 字段常用 package://robot_description/meshes/...


8. 数据流示例

1
2
3
4
5
6
7
8
9
URDF: package://my_robot/meshes/base.dae
↓ robot_state_publisher / rviz 读取
↓ resource_retriever::Retriever::get()
↓ ament_index: my_robot → /install/my_robot/share/my_robot
↓ file:///install/my_robot/share/my_robot/meshes/base.dae
↓ libcurl 读入 MemoryResource
↓ AssimpLoader::ResourceIOSystem::Open()
↓ 解析 dae + 相对路径纹理 package://my_robot/textures/...
↓ Ogre::Mesh → rviz 渲染

9. 测试覆盖

C++ gtest (test/test.cpp)

用例 验证
getByPackage package://resource_retriever/test/test.txt 内容为 'A'
http http://packages.ros.org/ros.key 非空
invalidFiles 无效 file、缺路径 package、不存在包、空包名

Python pytest (test/test.py)

与 C++ 基本对应;差异在于无效包名时 Python 直接抛出 PackageNotFoundError(来自 get_package_share_directory),C++ 则包装为 resource_retriever::Exception


10. 构建与导出

1
2
3
4
5
6
7
add_library(${PROJECT_NAME} src/retriever.cpp)
ament_target_dependencies(${PROJECT_NAME}
ament_index_cpp
libcurl_vendor
)
ament_python_install_package(${PROJECT_NAME} ...)
ament_export_targets(${PROJECT_NAME} HAS_LIBRARY_TARGET)

依赖:

1
2
3
resource_retriever
├── ament_index_cpp / ament_index_python
└── libcurl_vendor → libcurl

11. 设计特点与局限

特点 说明
URI 统一入口 本地包资源与远程 URL 同一 API
零落盘 全内存加载,适合嵌入式/只读环境
轻量 无 ROS 节点、无 rclcpp 依赖
连接复用 Retriever 实例复用 curl easy handle
局限 说明
同步阻塞 get() 阻塞直到完成,无 async API
无缓存 每次 get() 重新读取;rviz 在上层用 QPixmapCache/Ogre 缓存
无 ROS 参数/重映射 不做 topic/service 式重映射
C++/Python 异常不一致 无效包名异常类型不同
每次调用新建 Retriever rviz 多处局部创建,未全局共享 handle
双缓冲拷贝 vector → shared_ptr 数组,多一次 memcpy

12. 与 ament_index 的关系

1
2
3
4
package://foo/bar/baz.stl
│ └─ 相对 share 的路径
└─ ament_index_cpp::get_package_share_directory("foo")
→ $AMENT_PREFIX_PATH/.../share/foo

pluginlib 的 index 机制不同:这里只用 包 share 目录查找,不涉及插件 XML 资源类型。


13. 推荐阅读顺序

  1. API 面retriever.hpp__init__.py
  2. 核心逻辑retriever.cppget()curlWriteFunc
  3. 依赖libcurl_vendor/CMakeLists.txt + libcurl_vendor-extras.cmake.in
  4. 集成示例
    • 简单:rviz_common/load_resource.cpp
    • mesh:rviz_rendering/mesh_loader.cpp
    • Assimp IO:assimp_loader.cppResourceIOSystem
    • URDF 纹理:robot_link.cpp
  5. 测试test/test.cpp 理解边界条件

14. 小结

resource_retriever 是 ROS 2 生态中的小型 URI→内存 适配器package:// 交给 ament_index,其余协议交给 libcurl(C++)或 urllib(Python)。本身不做解析 mesh/图像,只负责按 URI 取字节;rviz、Marker、URDF 可视化在上层解释这些字节。

如果你希望,我可以把本文写入 ros2doc/ros/resource_retriever 源码详细分析.md,或继续分析 AssimpLoader 如何把 MemoryResource 转成 Ogre Mesh 的完整链路。

robot_state_publisher 源码详细分析

robot_state_publisher 源码详细分析

工作区路径:/home/cp/work2/ros2Learn/ros2_humble/src/ros/robot_state_publisher
版本:3.0.3,许可证 BSD

robot_state_publisher 是 ROS 2 中连接 URDF 运动学模型tf2 坐标系树 的核心节点:启动时加载 URDF,订阅 joint_states,用 KDL 计算各 link 相对位姿并发布到 /tf/tf_static。rviz、MoveIt、导航栈等都依赖它提供的 TF 树。


1. 仓库结构

1
2
3
4
5
6
7
8
9
10
11
robot_state_publisher/
├── include/robot_state_publisher/
│ └── robot_state_publisher.hpp # RobotStatePublisher 类声明
├── src/
│ └── robot_state_publisher.cpp # 全部实现(~428 行)
├── launch/ # URDF/xacro 加载示例(8 个 launch 文件)
├── urdf/ # 示例 URDF
├── test/ # launch_testing + gtest 集成测试
├── CMakeLists.txt
├── package.xml
└── README.md

特点:单节点、单源文件;通过 rclcpp_components 注册为可组合组件。


2. 在 ROS 2 栈中的位置

flowchart TB
  subgraph input [输入]
    URDF["robot_description 参数\n(URDF XML)"]
    JS["/joint_states\nsensor_msgs/JointState"]
  end

  subgraph rsp [robot_state_publisher]
    PARSE["urdf::Model + kdl_parser"]
    SEG["segments_ / segments_fixed_"]
    FK["KDL::Segment::pose(q)"]
    TF["TransformBroadcaster"]
    STF["StaticTransformBroadcaster"]
  end

  subgraph output [输出]
    RD["/robot_description\nstd_msgs/String"]
    TFOUT["/tf"]
    STFOUT["/tf_static"]
  end

  subgraph consumers [消费者]
    RVIZ[rviz]
    MOVEIT[MoveIt]
    NAV[nav2 / slam]
  end

  URDF --> PARSE --> SEG
  JS --> FK
  SEG --> FK
  FK --> TF --> TFOUT
  SEG --> STF --> STFOUT
  URDF --> RD
  TFOUT --> consumers
  STFOUT --> consumers
角色 说明
上游 launch 文件设置 robot_descriptionjoint_state_publisher / 控制器发布关节角
本包 URDF → KDL 树 → 逐关节 TF
下游 tf2 监听者(rviz、MoveIt、AMCL 等)

3. 类设计

3.1 SegmentPair

1
2
3
4
5
6
7
8
9
10
11
12
13
class SegmentPair final
{
public:
explicit SegmentPair(
const KDL::Segment & p_segment,
const std::string & p_root,
const std::string & p_tip)
: segment(p_segment), root(p_root), tip(p_tip) {}

KDL::Segment segment;
std::string root; ///< 父 link 名
std::string tip; ///< 子 link 名
};

每个可动/固定关节对应一条 TF 边:roottip,几何由 KDL Segment 描述。

3.2 RobotStatePublisher 核心成员

成员 类型 作用
segments_ map<string, SegmentPair> 可动关节(revolute/continuous/prismatic 等)
segments_fixed_ map<string, SegmentPair> 固定关节
mimic_ MimicMap mimic 关节映射
tf_broadcaster_ TransformBroadcaster 发布 /tf
static_tf_broadcaster_ StaticTransformBroadcaster 发布 /tf_static
description_pub_ Publisher<String> 转发 URDF 文本
joint_state_sub_ Subscription<JointState> 订阅 /joint_states
last_publish_time_ map<string, Time> 各关节上次发布时间(节流)

4. 启动与参数

4.1 构造函数流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
RobotStatePublisher::RobotStatePublisher(const rclcpp::NodeOptions & options)
: rclcpp::Node("robot_state_publisher", options)
{
std::string urdf_xml = this->declare_parameter("robot_description", std::string(""));
// 空则尝试从命令行读 URDF 文件(已废弃)
...
double publish_freq = this->declare_parameter("publish_frequency", 20.0);
this->declare_parameter("frame_prefix", "");
this->declare_parameter("ignore_timestamp", false);

tf_broadcaster_ = std::make_unique<tf2_ros::TransformBroadcaster>(this);
static_tf_broadcaster_ = std::make_unique<tf2_ros::StaticTransformBroadcaster>(this);

description_pub_ = this->create_publisher<std_msgs::msg::String>(
"robot_description", rclcpp::QoS(1).transient_local());

setupURDF(urdf_xml);

joint_state_sub_ = this->create_subscription<sensor_msgs::msg::JointState>(
"joint_states", rclcpp::SensorDataQoS(), ...);

publishFixedTransforms();
// 参数回调
param_cb_ = add_on_set_parameters_callback(...);
parameter_subscription_ = rclcpp::AsyncParametersClient::on_parameter_event(...);
}

4.2 参数一览

参数 类型 默认 说明
robot_description string 必填 URDF XML 全文
publish_frequency double 20.0 可动 TF 最大发布频率 (Hz),范围 0–1000
frame_prefix string "" 所有 frame_id 前缀(多机器人场景)
ignore_timestamp bool false true 时忽略时间戳节流,每条 joint_states 都发布

4.3 话题

方向 话题 类型 QoS
发布 robot_description std_msgs/String transient_local
发布 /tf tf2_msgs/TFMessage 默认
发布 /tf_static tf2_msgs/TFMessage transient_local
订阅 joint_states sensor_msgs/JointState SensorDataQoS

5. URDF 解析与 KDL 树构建

5.1 parseURDF

1
2
3
4
5
6
7
8
9
10
11
KDL::Tree RobotStatePublisher::parseURDF(const std::string & urdf_xml, urdf::Model & model)
{
if (!model.initString(urdf_xml)) {
throw std::runtime_error("Unable to initialize urdf::model from robot description");
}
KDL::Tree tree;
if (!kdl_parser::treeFromUrdfModel(model, tree)) {
throw std::runtime_error("Failed to extract kdl tree from robot description");
}
return tree;
}

两步转换:URDF XML → urdf::Model → KDL::Tree(依赖 kdl_parser + orocos_kdl)。

5.2 setupURDF

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void RobotStatePublisher::setupURDF(const std::string & urdf_xml)
{
urdf::Model model;
KDL::Tree tree = parseURDF(urdf_xml, model);

// 构建 mimic 映射(显式拷贝 JointMimic,避免悬空引用)
mimic_.clear();
for (...) {
if (i.second->mimic) {
auto jm = std::make_shared<urdf::JointMimic>();
jm->offset = i.second->mimic->offset;
jm->multiplier = i.second->mimic->multiplier;
jm->joint_name = i.second->mimic->joint_name;
mimic_[i.first] = jm;
}
}

segments_.clear();
segments_fixed_.clear();
addChildren(model, tree.getRootSegment());

// 发布 URDF 到 /robot_description
description_pub_->publish(...);
}

5.3 addChildren:关节分类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void RobotStatePublisher::addChildren(...)
{
const std::string & root = GetTreeElementSegment(segment->second).getName();
for (children ...) {
const KDL::Segment & child = GetTreeElementSegment(children[i]->second);
SegmentPair s(..., root, child.getName());
if (child.getJoint().getType() == KDL::Joint::None) {
if (model.getJoint(...) && ...->type == urdf::Joint::FLOATING) {
// 浮动关节:不加入任何 map
} else {
segments_fixed_.insert(...); // 固定关节 → tf_static
}
} else {
segments_.insert(...); // 可动关节 → /tf
}
addChildren(model, children[i]);
}
}

分类规则:

URDF/KDL 关节类型 去向 TF 话题
fixed segments_fixed_ /tf_static
revolute / continuous / prismatic segments_ /tf
floating 跳过 不发布
mimic 不单独分类 运行时从被 mimic 关节推导

kdl_parser 会把 fixed 关节映射为 KDL::Joint::None,可动关节保留 1-DOF。


6. TF 发布核心

6.1 KDL → geometry_msgs

1
2
3
4
5
6
7
8
geometry_msgs::msg::TransformStamped kdlToTransform(const KDL::Frame & k)
{
geometry_msgs::msg::TransformStamped t;
t.transform.translation.x = k.p.x();
...
k.M.GetQuaternion(t.transform.rotation.x, ...);
return t;
}

6.2 可动关节:publishTransforms

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void RobotStatePublisher::publishTransforms(
const std::map<std::string, double> & joint_positions,
const builtin_interfaces::msg::Time & time)
{
std::string frame_prefix = get_parameter("frame_prefix").get_value<std::string>();
for (const auto & jnt : joint_positions) {
auto seg = segments_.find(jnt.first);
if (seg != segments_.end()) {
geometry_msgs::msg::TransformStamped tf_transform =
kdlToTransform(seg->second.segment.pose(jnt.second));
tf_transform.header.stamp = time;
tf_transform.header.frame_id = frame_prefix + seg->second.root;
tf_transform.child_frame_id = frame_prefix + seg->second.tip;
tf_transforms.push_back(tf_transform);
}
}
tf_broadcaster_->sendTransform(tf_transforms);
}

要点:

  • 只使用 positionvelocity / effort 被忽略
  • 每条 TF 是 单段 变换:parent_link → child_link
  • segment.pose(q) 用关节角 q 计算该段位姿(含 joint origin 的固定偏移)

示例 URDF(continuous 关节,origin xyz=”5 0 0” rpy=”0 0 1.57”):

1
joint1 转 π 弧度 → link1→link2 的 TF translation.x ≈ 5.0

(集成测试 test_two_links_moving_joint.cpp 验证)

6.3 固定关节:publishFixedTransforms

1
2
3
4
5
6
7
8
9
10
void RobotStatePublisher::publishFixedTransforms()
{
for (const auto & seg : segments_fixed_) {
geometry_msgs::msg::TransformStamped tf_transform =
kdlToTransform(seg.second.segment.pose(0)); // q=0
tf_transform.header.stamp = now;
...
}
static_tf_broadcaster_->sendTransform(tf_transforms);
}

启动时发布一次;URDF 热更新后也会重新调用。


7. joint_states 回调逻辑

7.1 主流程

1
2
3
4
5
6
7
8
9
10
void RobotStatePublisher::callbackJointState(...)
{
// 1. 校验 name.size == position.size
// 2. 检测时间回退(bag 回放)→ 清空 last_publish_time_
// 3. 节流判断
// 4. 构建 joint_positions map
// 5. 处理 mimic 关节
// 6. publishTransforms
// 7. 更新 last_publish_time_
}

7.2 Mimic 关节

1
2
3
4
5
6
7
for (const auto & i : mimic_) {
if (joint_positions.find(i.second->joint_name) != joint_positions.end()) {
double pos = joint_positions[i.second->joint_name] * i.second->multiplier +
i.second->offset;
joint_positions.insert(std::make_pair(i.first, pos));
}
}

公式:q_mimic = q_source × multiplier + offset

mimic 关节名必须在 segments_ 中有对应段才会发布 TF;若 mimic 的是 fixed 关节,则该 mimic 关节本身也是 fixed,不会进入 segments_

7.3 发布频率节流

1
2
3
4
5
6
7
rclcpp::Time current_time(state->header.stamp);
double publish_freq = this->get_parameter("publish_frequency").get_value<double>();
std::chrono::milliseconds publish_interval_ms =
std::chrono::milliseconds(static_cast<uint64_t>(1000.0 / publish_freq));
rclcpp::Time max_publish_time = last_published + rclcpp::Duration(publish_interval_ms);
if (get_parameter("ignore_timestamp").get_value<bool>() ||
current_time.nanoseconds() >= max_publish_time.nanoseconds())

逻辑:

  • 取所有关节中 最早last_publish_time 作为 last_published
  • 仅当消息 header.stamp >= last_published + 1/freq 时才发布
  • 这是基于 消息时间戳 的节流,不是 wall-clock 定时器
  • ignore_timestamp=true 时每条消息都发布

7.4 时间回退处理

bag 回放若时间戳倒退,会清空 last_publish_time_ 并警告,避免 TF 被错误节流。


8. 动态 URDF 更新

两套机制配合:

8.1 参数校验(同步)

1
2
3
4
5
6
7
8
rcl_interfaces::msg::SetParametersResult RobotStatePublisher::parameterUpdate(...)
{
if (parameter.get_name() == "robot_description") {
if (new_urdf.empty()) { result.successful = false; ... }
try { parseURDF(new_urdf, dummy_model); } catch (...) { result.successful = false; }
}
...
}

8.2 参数事件(异步应用)

1
2
3
4
5
6
7
void RobotStatePublisher::onParameterEvent(...)
{
if (event->node != this->get_fully_qualified_name()) return;
// 过滤 robot_description CHANGED
setupURDF(it.second->value.string_value);
publishFixedTransforms();
}

更新后:

  1. 重建 segments_ / segments_fixed_ / mimic_
  2. 重新发布 /robot_description
  3. 重新发布 /tf_static

可动 TF 需等待新的 joint_states 才会更新。测试 test_two_links_change_fixed_joint.cpp 验证 fixed 关节从 xyz=5 改为 xyz=10 后 /tf_static 变化。

v3.0.3 修复了带 mimic 关节 URDF 重载时的崩溃(显式拷贝 JointMimic)。


9. 组件化部署

1
2
3
rclcpp_components_register_node(${PROJECT_NAME}_node
PLUGIN "robot_state_publisher::RobotStatePublisher"
EXECUTABLE robot_state_publisher)

两种运行方式:

1
2
3
4
5
# 独立可执行文件
ros2 run robot_state_publisher robot_state_publisher

# 组件容器内加载
ros2 component standalone robot_state_publisher robot_state_publisher::RobotStatePublisher

10. Launch 示例模式

launch/rsp-launch-urdf-file1.py 典型写法:

1
2
3
4
5
6
with open(urdf_file, 'r') as infp:
robot_desc = infp.read()
params = {'robot_description': robot_desc}
Node(package='robot_state_publisher',
executable='robot_state_publisher',
parameters=[params])

其他 launch 示例:

  • 内联 URDF 字符串
  • xacro 命令行 / API / Command substitution
  • XML launch 格式

xacro 处理在 launch 层 完成,节点只接收最终 URDF 字符串。


11. 依赖关系

1
2
3
4
5
6
7
8
9
10
robot_state_publisher
├── urdf # URDF 解析 (urdf::Model)
├── kdl_parser # URDF → KDL::Tree
├── orocos_kdl # KDL::Segment::pose()
├── tf2_ros # TransformBroadcaster / StaticTransformBroadcaster
├── rclcpp # Node、参数、订阅
├── rclcpp_components # 组件注册
├── sensor_msgs # JointState
├── geometry_msgs # TransformStamped
└── std_msgs # robot_description

12. 测试覆盖

测试 验证点
test_two_links_fixed_joint 固定关节 → /tf_static
test_two_links_fixed_joint_prefix frame_prefix 前缀
test_two_links_moving_joint continuous 关节 + joint_states → /tf
test_two_links_change_fixed_joint 运行时更新 URDF,fixed TF 变化
test_change_mimic_joint mimic 关节 URDF 热更新

13. 设计特点与局限

特点 说明
逐关节 TF 每条边独立发布,不做整树 FK 连乘
仅 1-DOF pose(double) 只支持单自由度关节
position only 忽略 velocity/effort
floating 不支持 6-DOF 浮动基座需其他节点发布
轻量 单文件 ~430 行,逻辑清晰
URDF 即参数 支持运行时热更新
局限 说明
无多 DOF 关节 planar / floating 等需特殊处理
节流基于消息 stamp 与 wall time 无关,bag 回放需 ignore_timestamp 或理解 stamp 逻辑
static TF 更新 URDF 变更后旧 /tf_static 不会自动“删除”旧 frame
mimic 仅处理 position 不传播 velocity

14. 典型数据流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
launch 读取 robot.urdf / xacro
↓ 设置 robot_description 参数
robot_state_publisher 启动
↓ urdf::Model + kdl_parser → KDL::Tree
↓ addChildren → segments_ / segments_fixed_
↓ publishFixedTransforms() → /tf_static
↓ publish robot_description (transient_local)

joint_state_publisher / controller
↓ /joint_states {name, position}
callbackJointState
↓ mimic 计算
↓ segment.pose(q) → TransformStamped
↓ /tf

rviz / MoveIt / nav2
↓ tf2 Buffer 查询 link 位姿

15. 推荐阅读顺序

  1. README.md — 话题、参数、关节分类
  2. 构造函数 — 初始化顺序
  3. setupURDF + addChildren — URDF 如何变成 segment map
  4. callbackJointState + publishTransforms — 核心 FK→TF 链路
  5. kdl_parser 分析 — URDF 到 KDL 的转换细节
  6. 测试two_links_moving_joint.urdf + test_two_links_moving_joint.cpp
  7. launch 示例 — 如何在 bringup 中传入 URDF

16. 小结

robot_state_publisher 是 ROS 2 运动学链路的 URDF → tf2 桥接节点:用 kdl_parser 建 KDL 树,按关节类型分流到 /tf/tf_static,订阅 joint_states 驱动可动关节变换,并支持 mimic 关节与 URDF 热更新。本身不做碰撞检测、逆运动学或整树 FK,职责单一、边界清晰。

如果你希望,我可以把本文写入 ros2doc/ros/robot_state_publisher 源码详细分析.md,或继续分析 rviz 如何用 TF + robot_description 渲染 RobotModel

ros_environment 源码详细分析

ros_environment 源码详细分析

工作区路径:/home/cp/work2/ros2Learn/ros2_humble/src/ros/ros_environment
版本:3.2.2,许可证 Apache 2.0

ros_environment 是 ROS 2 中最底层的“元数据包”之一:不含任何 C++/Python 源码或可执行文件,仅通过 ament environment hookssource install/setup.bash 时注入 4 个全局环境变量。几乎所有 ROS 2 工作流都隐式依赖这些变量,但很少直接 #include 或 import 这个包。


1. 仓库结构

1
2
3
4
5
6
7
8
9
10
ros_environment/
├── CMakeLists.txt # 唯一“逻辑”文件:定义变量 + 注册 hooks
├── package.xml
├── LICENSE
└── env-hooks/ # 环境钩子模板(.in → configure_file 展开)
├── 0.ros_distro_check.sh.in # 发行版混用警告(仅 Unix)
├── 1.ros_distro.{sh,dsv,bat}.in
├── 1.ros_version.{sh,dsv,bat}.in
├── 1.ros_python_version.{sh,dsv,bat}.in
└── 1.ros_localhost_only.{sh,dsv,bat}.in

特点:16 个文件,零运行时代码project(ros_environment NONE) 表示 CMake 不启用任何语言编译。


2. 架构与激活路径

flowchart TB
  subgraph build [构建时 CMakeLists.txt]
    VARS["设置 ROS_DISTRO / ROS_VERSION\nROS_PYTHON_VERSION / ROS_LOCALHOST_ONLY"]
    HOOKS["ament_environment_hooks()"]
    INSTALL["安装到 share/ros_environment/environment/"]
  end

  subgraph source [用户 source setup.bash]
    SETUP["install/setup.bash"]
    UTIL["_local_setup_util.py\n解析各包 .dsv"]
    SH["可选:source *.sh hooks"]
    ENV["导出环境变量"]
  end

  subgraph consumers [运行时消费者]
    RCL["rcl: ROS_LOCALHOST_ONLY"]
    DOCTOR["ros2 doctor: ROS_DISTRO"]
    BAG["rosbag2: 写入 bag 元数据"]
    RUST["rosidl_generator_rs: 版本分支"]
  end

  VARS --> HOOKS --> INSTALL
  SETUP --> UTIL --> ENV
  SETUP --> SH --> ENV
  ENV --> consumers
阶段 行为
colcon build CMake configure_file@ROS_DISTRO@ 等占位符替换为实际值,安装 hook 文件
source setup.bash ament 按拓扑序合并所有包的 hooks,设置环境变量
运行时 各包通过 getenv / os.environ 读取

3. CMakeLists.txt 核心逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
cmake_minimum_required(VERSION 3.5)
project(ros_environment NONE)
find_package(ament_cmake_core REQUIRED)

set(ROS_LOCALHOST_ONLY "0")
set(ROS_VERSION "2")
set(ROS_PYTHON_VERSION "3")

# allow overriding the distro name
if(DEFINED ENV{ROS_DISTRO_OVERRIDE})
set(ROS_DISTRO $ENV{ROS_DISTRO_OVERRIDE})
else()
set(ROS_DISTRO "humble")
endif()

set(
hooks
"1.ros_distro"
"1.ros_localhost_only"
"1.ros_python_version"
"1.ros_version"
)
set(shells "dsv")
if(CMAKE_HOST_UNIX)
list(APPEND shells "sh")
else()
list(APPEND shells "bat")
endif()
foreach(hook ${hooks})
foreach(shell ${shells})
ament_environment_hooks(
"${CMAKE_CURRENT_SOURCE_DIR}/env-hooks/${hook}.${shell}.in")
endforeach()
endforeach()
if(CMAKE_HOST_UNIX)
ament_environment_hooks(
"${CMAKE_CURRENT_SOURCE_DIR}/env-hooks/0.ros_distro_check.sh.in")
endif()

ament_package()

3.1 编译期常量

CMake 变量 Humble 默认值 含义
ROS_DISTRO "humble" 发行版名称
ROS_VERSION "2" ROS 主版本号
ROS_PYTHON_VERSION "3" Python 主版本
ROS_LOCALHOST_ONLY "0" 是否仅本机通信

3.2 ROS_DISTRO_OVERRIDE

构建时若设置环境变量 ROS_DISTRO_OVERRIDE,则覆盖 ROS_DISTRO 的 baked-in 值。用于 fork 发行版或 CI 自定义标签,而源码树仍是 humble。

3.3 Hook 格式选择

  • 所有 hook 都有 .dsv.in(ament 现代路径,由 _local_setup_util.py 处理)
  • Unix 额外生成 .sh.in
  • Windows 额外生成 .bat.in
  • 0.ros_distro_check.sh.in 仅 Unix,且无前缀 1.,保证在设置 ROS_DISTRO 之前执行检查

文件名前缀 0. / 1. 控制同一包内 hook 的字母序执行顺序


4. 四个环境变量详解

4.1 ROS_DISTRO

强制设置(覆盖已有值):

1
2
3
# generated from ros_environment/env-hooks/1.ros_distro.sh.in

export ROS_DISTRO=@ROS_DISTRO@
1
set;ROS_DISTRO;@ROS_DISTRO@

混用检测(仅 sh,在设置前执行):

1
2
3
4
5
# generated from ros_environment/env-hooks/0.ros_distro.sh.in

if [ -n "$ROS_DISTRO" -a "$ROS_DISTRO" != "@ROS_DISTRO@" ]; then
echo "ROS_DISTRO was set to '$ROS_DISTRO' before. Please make sure that the environment does not mix paths from different distributions." >&2
fi

若环境中已有不同的 ROS_DISTRO(例如先 source 了 Foxy 又 source Humble),会打印 stderr 警告,防止 AMENT_PREFIX_PATH 混用。

典型消费者

  • ros2 doctor — 校验 ROS_DISTRO 是否设置
  • rosbag2 — 写入 bag 元数据 ROS_DISTRO
  • rosidl_generator_rs — 按发行版选择不同 API:
1
2
3
4
if os.environ['ROS_DISTRO'] <= 'humble':
import rosidl_cmake as rosidl_pycommon
else:
import rosidl_pycommon

4.2 ROS_VERSION

1
set;ROS_VERSION;@ROS_VERSION@

固定为 "2",用于区分 ROS 1 / ROS 2。Humble 工作区内几乎没有 C++ 代码读取此变量,主要供 shell 脚本、文档和跨版本工具使用。

4.3 ROS_PYTHON_VERSION

1
set;ROS_PYTHON_VERSION;@ROS_PYTHON_VERSION@

固定为 "3"。ament/colcon 的 Python 包安装路径由其他 hook 管理;此变量提供显式版本标识,供构建脚本或第三方工具判断 Python 系列。

4.4 ROS_LOCALHOST_ONLY

控制 RMW 是否限制在本机 loopback 通信(安全/隔离场景)。

dsv(推荐路径)— 仅在未设置时写入

1
set-if-unset;ROS_LOCALHOST_ONLY;@ROS_LOCALHOST_ONLY@

bat — 同样仅在空时设置

1
2
3
4
5
REM generated from ros_environment/env-hooks/1.ros_localhost_only.bat.in

if "%ROS_LOCALHOST_ONLY%" == "" (
set ROS_LOCALHOST_ONLY=@ROS_LOCALHOST_ONLY@
)

sh — 逻辑与 dsv/bat 不一致

1
2
3
4
5
# generated from ros_environment/env-hooks/1.ros_localhost_only.sh.in

if [ -n "$ROS_LOCALHOST_ONLY" ]; then
export ROS_LOCALHOST_ONLY=@ROS_LOCALHOST_ONLY@
fi

这里 -n 表示“若已设置则覆盖为默认值 0”,与 set-if-unset 语义相反。现代 setup.bash 优先走 dsv 路径,因此实际行为以 dsv 为准;直接 source .sh hook 时可能出现意外覆盖。

运行时读取rcl):

1
2
3
4
5
6
7
8
9
10
11
rcl_ret_t
rcl_get_localhost_only(rmw_localhost_only_t * localhost_only)
{
...
get_env_error_str = rcutils_get_env(RCL_LOCALHOST_ENV_VAR, &ros_local_host_env_val);
...
*localhost_only = (ros_local_host_env_val != NULL &&
strcmp(ros_local_host_env_val, "1") == 0) ?
RMW_LOCALHOST_ONLY_ENABLED : RMW_LOCALHOST_ONLY_DISABLED;
return RCL_RET_OK;
}

仅当值为 "1" 时启用;未设置或其他值均视为 disabled(默认 0 来自 dsv 的 set-if-unset)。


5. ament environment hooks 机制

5.1 注册流程

ament_environment_hooks()ament_cmake_core)对每个 .in 模板:

  1. configure_file(... @ONLY) 展开 @ROS_DISTRO@
  2. 安装到 share/<package>/environment/<hook>.<ext>
  3. 记录到 _AMENT_CMAKE_ENVIRONMENT_HOOKS_<ext> 列表

ament_package() 时生成 local_setup.dsvpackage.dsv,供 workspace 级 setup.bash 聚合。

5.2 DSV 格式

类型 语法 ros_environment 用法
set set;VAR;value ROS_DISTRO, ROS_VERSION, ROS_PYTHON_VERSION
set-if-unset set-if-unset;VAR;value ROS_LOCALHOST_ONLY
source source;path/to/hook.sh 由 ament 自动生成,引用 sh/bat hooks

_local_setup_util.pyset-if-unset 实现:

1
2
3
4
5
6
7
def _set_if_unset(name, value):
global env_state
line = FORMAT_STR_SET_ENV_VAR.format_map(
{'name': name, 'value': value})
if env_state.get(name, os.environ.get(name)):
line = FORMAT_STR_COMMENT_LINE.format_map({'comment': line})
return [line]

若变量已有值,对应 export 行会被注释掉,不会覆盖用户设置。


6. 安装后的文件布局

构建安装后(示意):

1
2
3
4
5
6
7
8
9
install/ros_environment/share/ros_environment/
├── environment/
│ ├── 0.ros_distro_check.sh
│ ├── 1.ros_distro.sh / .dsv / .bat
│ ├── 1.ros_version.sh / .dsv / .bat
│ ├── 1.ros_python_version.sh / .dsv / .bat
│ └── 1.ros_localhost_only.sh / .dsv / .bat
├── local_setup.dsv # 本包 hook 索引
└── package.dsv # 指向 local_setup.*

workspace 根目录的 install/setup.bash 会遍历所有已安装包的 package.dsv,最终合并环境。

验证命令:

1
2
3
source /path/to/install/setup.bash
echo $ROS_DISTRO $ROS_VERSION $ROS_PYTHON_VERSION $ROS_LOCALHOST_ONLY
# 期望: humble 2 3 0

7. 依赖关系

7.1 本包依赖

1
2
ros_environment
└── ament_cmake_core # 仅需 CMake 基础设施

7.2 谁依赖本包

在本工作区中显式声明依赖较少:

依赖类型 用途
ros2doctor exec_depend 检查 ROS_DISTRO
rosidl_generator_rs buildtool_depend 构建时读 ROS_DISTRO

实际上,只要 workspace 安装了 ros_environment 并 source setup,所有节点都能读到这些环境变量,无需在 package.xml 中声明。


8. 与其他“环境类”包的对比

设置的主要变量 作用
ros_environment ROS_DISTRO, ROS_VERSION, ROS_PYTHON_VERSION, ROS_LOCALHOST_ONLY ROS 身份与网络策略元数据
ament_package 生成的 setup AMENT_PREFIX_PATH, PYTHONPATH, PATH, LD_LIBRARY_PATH 包路径与库搜索
libcurl_vendor 等 vendor 包 特定库的 LD_LIBRARY_PATH 第三方库运行时路径

ros_environment 不修改 PATHAMENT_PREFIX_PATH,只注入语义标识类变量。


9. 设计特点

特点 说明
构建期 bake-in ROS_DISTRO=humble 在编译时写入 hook,非运行时探测
跨平台 sh / bat / dsv 三套格式
强制 vs 可选 ROS_DISTRO 强制;ROS_LOCALHOST_ONLY 尊重用户预设
混源保护 0.ros_distro_check 警告多发行版混用
零运行时 无库、无节点、无 Python 模块
可覆盖构建标签 ROS_DISTRO_OVERRIDE 支持自定义发行版名

10. 局限与注意事项

说明
必须 source setup 未 source 时 ROS_DISTRO 为空,ros2 doctor 等会报错
与 underlay 叠加 overlay workspace source 后,ROS_DISTRO 由 overlay 中的 ros_environment 决定
sh/bat localhost 不一致 .sh hook 的 -n 条件疑似笔误,实际以 dsv 为准
package.xml 描述不全 描述只提 ROS_VERSIONROS_DISTRO,未提 Python/localhost
无单元测试 包内无 test 目录,行为依赖 ament 集成测试间接验证

11. 典型使用场景

场景 1:正常开发

1
2
3
source /opt/ros/humble/setup.bash
# ROS_DISTRO=humble, ROS_VERSION=2
colcon build && source install/setup.bash

场景 2:隔离网络测试

1
2
3
export ROS_LOCALHOST_ONLY=1   # 在 source 前设置,dsv 不会覆盖
source install/setup.bash
# DDS 流量限制在 localhost

场景 3:自定义发行版 fork

1
2
3
export ROS_DISTRO_OVERRIDE=mycompany_ros
colcon build --packages-select ros_environment
# 安装后 ROS_DISTRO=mycompany_ros

12. 推荐阅读顺序

  1. CMakeLists.txt — 四个变量默认值与 hook 注册
  2. env-hooks/*.dsv.in — 现代 setup 路径的实际语义
  3. 0.ros_distro_check.sh.in — 混用警告逻辑
  4. ament_environment_hooks.cmake — hook 如何安装与索引
  5. _local_setup_util.pyset / set-if-unset 如何生成 shell 命令
  6. rcl/localhost.cROS_LOCALHOST_ONLY 的运行时效果

13. 小结

ros_environment 是 ROS 2 的环境变量注入包:通过 ament hooks 在 workspace 激活时设置 ROS_DISTRO=humbleROS_VERSION=2ROS_PYTHON_VERSION=3,并在未预设时默认 ROS_LOCALHOST_ONLY=0。代码量极小,却是 ROS 2 生态的“身份标识层”——让工具链、bag 录制、doctor 诊断、Rust 代码生成等能知道当前运行的是哪个发行版、哪个 ROS 世代。

如果你希望,我可以把本文写入 ros2doc/ros/ros_environment 源码详细分析.md,或继续分析 ament setup.bash 如何把数百个包的 hooks 合并成最终环境