ROS2系列教程:package结构(CMake与Python两种包)
本文摘要
ROS2 内置的std_msgs覆盖常见场景,但你的业务数据(比如"机器人位姿 + 置信度 + 时间戳")往往是组合结构。这时需要自定义消息类型——定义一个.msg文件,rosidl…
本文是 ROS2 系列教程的第 4 篇
本文是 ROS2 系列教程的第 4 篇:package 结构剖析(CMake 与 Python 两种包)。上一篇我们创建了两个包,但只是“会跑”;本篇把包的每一块骨头拆开看:
package.xml每个字段的含义、CMakeLists.txt与setup.py的配置细节、自定义消息/服务接口包(.msg/.srv)的定义与生成,以及两种包类型的完整对比。学完本篇,你能看懂任何开源 ROS2 包的目录结构,也能正确声明依赖、定义自己的接口。
一、package.xml:包的身份证
1.1 最小完整示例
package.xml 是每个 ROS2 包都必须有的清单文件,描述包的元数据与依赖。一个完整的 package.xml 长这样:
<?xml version="1.0"?><?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?><package format="3"> <!-- 基本信息 --> <name>my_pkg</name> <version>0.1.0</version> <description>一个示例包</description> <maintainer email="you@example.com">Your Name</maintainer> <license>Apache-2.0</license>
<!-- 依赖声明(五种) --> <buildtool_depend>ament_cmake</buildtool_depend> <!-- 构建工具 --> <depend>rclcpp</depend> <!-- 构建+运行时依赖 --> <build_depend>sensor_msgs</build_depend> <!-- 仅构建时依赖 --> <exec_depend>launch_ros</exec_depend> <!-- 仅运行时依赖 --> <test_depend>ament_lint_auto</test_depend> <!-- 测试依赖 -->
<!-- 导出(可省略) --> <export> <build_type>ament_cmake</build_type> </export></package>1.2 五种依赖类型详解
| 标签 | 含义 | 何时使用 |
|---|---|---|
<buildtool_depend> |
构建工具本身 | 几乎都是 ament_cmake 或 ament_python |
<depend> |
构建 + 运行时都需要 | 最常用,如 rclcpp、rclpy、std_msgs |
<build_depend> |
仅编译时需要 | 只在头文件/编译期用到的库 |
<exec_depend> |
仅运行时需要 | 如 launch 文件依赖、Python 包 |
<test_depend> |
测试时依赖 | ament_lint_auto 等 |
最佳实践:拿不准时统一用 <depend>,它等价于同时声明 build + exec 依赖,最保险。
1.3 format 属性:3 与 2 的区别
<package format="3">format="2":最常用,支持<depend>。format="3":额外支持<member_of_group>(把包归入组,如rosidl_interface_packages),用于接口包。format="1":老格式,只有 build/exec 两种依赖,新包不要用。
接口包(定义 .msg/.srv 的包)必须用 format 3,并在 export 里声明组:
<export> <build_type>ament_cmake</build_type> <member_of_group>rosidl_interface_packages</member_of_group></export>二、Python 包结构详解
2.1 完整目录树
py_pkg/├── package.xml # 包清单(build_type = ament_python)├── setup.py # 构建配置(核心)├── setup.cfg # 安装路径配置├── resource/│ └── py_pkg # ament 索引标记(内容可空)├── test/│ ├── test_copyright.py│ ├── test_flake8.py│ └── test_pep257.py└── py_pkg/ # Python 源码目录(与包同名) ├── __init__.py └── my_node.py # 节点实现2.2 setup.py 逐字段
from setuptools import setupfrom glob import globimport os
package_name = 'py_pkg'
setup( name=package_name, # 包名,必须与目录同名 version='0.0.0', # 版本号 packages=[package_name], # Python 包列表(源码目录) data_files=[ # 数据/资源文件安装位置 ('share/ament_index/resource_index/packages', ['resource/' + package_name]), # ament 索引(必须) ('share/' + package_name, ['package.xml']), # 包清单(必须) (os.path.join('share', package_name, 'launch'), # launch 文件(可选) glob('launch/*.launch.py')), (os.path.join('share', package_name, 'config'), # 配置文件(可选) glob('config/*.yaml')), ], install_requires=['setuptools'], zip_safe=True, maintainer='your_name', maintainer_email='you@example.com', description='Python 示例包', license='Apache-2.0', entry_points={ 'console_scripts': [ 'my_node = py_pkg.my_node:main', # 命令名 = 模块:函数 ], },)四个必须:name 与目录同名、packages 含源码目录、data_files 含 ament 索引与 package.xml、entry_points 定义可执行命令。缺任何一个,ros2 run 都可能失败。
2.3 setup.cfg:安装路径
[develop]script_dir=$base/lib/py_pkg[install]install_scripts=$base/lib/py_pkg这个文件把脚本安装到 lib/<包名>/ 目录,ros2 run 才能找到。一般照抄即可,不要乱改。
三、CMake 包结构详解
3.1 完整目录树
cpp_pkg/├── package.xml # 包清单(build_type = ament_cmake)├── CMakeLists.txt # 构建规则(核心)├── include/cpp_pkg/ # 公共头文件(按约定与包同名)│ └── my_node.hpp├── src/│ ├── my_node.cpp # 节点实现│ └── main.cpp # 入口├── launch/ # launch 文件(需在 CMakeLists 安装)├── config/ # 配置文件└── msg/ srv/ action/ # 接口定义目录(本包定义接口时)3.2 CMakeLists.txt 逐段
cmake_minimum_required(VERSION 3.8)project(cpp_pkg) # 项目名 = 包名
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") add_compile_options(-Wall -Wextra -Wpedantic)endif()
# 找依赖:用到几个找几个,保持最小化find_package(ament_cmake REQUIRED)find_package(rclcpp REQUIRED)find_package(std_msgs REQUIRED) # 用到标准消息时
# 生成库(可选,模块化时用)add_library(cpp_pkg_core src/my_node.cpp)target_include_directories(cpp_pkg_core PUBLIC $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include> $<INSTALL_INTERFACE:include>)ament_target_dependencies(cpp_pkg_core rclcpp std_msgs)
# 生成可执行文件add_executable(my_node src/main.cpp)target_link_libraries(my_node cpp_pkg_core) # 链接自己的库ament_target_dependencies(my_node rclcpp std_msgs)
# 安装:可执行文件、头文件、launch、config(缺一不可)install(TARGETS my_node cpp_pkg_core EXPORT export_${PROJECT_NAME} ARCHIVE DESTINATION lib LIBRARY DESTINATION lib RUNTIME DESTINATION lib/${PROJECT_NAME})install(DIRECTORY include/ DESTINATION include)install(DIRECTORY launch DESTINATION share/${PROJECT_NAME})install(DIRECTORY config DESTINATION share/${PROJECT_NAME})
ament_package()最容易踩的三个坑:
- 忘写
install(TARGETS ...)→ros2 run找不到命令。 - 忘写
ament_target_dependencies→ 编译报“找不到头文件/链接错误”。 - 忘写
find_package(std_msgs)却在代码里#include "std_msgs/msg/string.hpp"→ 编译报错。
3.3 头文件与源文件分离
工程化项目推荐把实现放库、入口放 main:
#pragma once#include "rclcpp/rclcpp.hpp"
class MyNode : public rclcpp::Node{public: MyNode();private: rclcpp::TimerBase::SharedPtr timer_;};#include "cpp_pkg/my_node.hpp"#include <chrono>
MyNode::MyNode() : Node("my_node"){ timer_ = this->create_wall_timer( std::chrono::seconds(1), [this]() { RCLCPP_INFO(this->get_logger(), "tick"); });}#include "cpp_pkg/my_node.hpp"#include "rclcpp/rclcpp.hpp"
int main(int argc, char** argv){ rclcpp::init(argc, argv); rclcpp::spin(std::make_shared<MyNode>()); rclcpp::shutdown(); return 0;}这样“库 + 薄入口”的结构便于单元测试与复用,是大型项目的标准组织方式。
四、自定义接口包:msg / srv
4.1 为什么要自定义接口
ROS2 内置的 std_msgs、sensor_msgs、geometry_msgs 覆盖常见场景,但你的业务数据(比如“机器人位姿 + 置信度 + 时间戳”)往往是组合结构。这时需要自定义消息类型——定义一个 .msg 文件,rosidl 工具会自动生成对应语言的类型代码。
4.2 创建接口包
接口包通常是独立的纯接口包(不放实现),因为它会被多个包依赖:
ros2 pkg create my_interfaces \ --build-type ament_cmake \ --license Apache-2.0 \ --maintainer-name your_name \ --maintainer-email you@example.com \ --description "Custom interfaces"4.3 定义 .msg 文件
在 my_interfaces/msg/ 目录创建 RobotStatus.msg:
# msg/RobotStatus.msg —— 自定义消息string robot_name # 字符串字段float32 battery_level # 浮点字段(0.0 ~ 1.0)int32 status_code # 整数状态码builtin_interfaces/Time stamp # 嵌套内置类型消息类型映射表(.msg 写法 → C++ 类型 → Python 类型):
| .msg 写法 | C++ 类型 | Python 类型 |
|---|---|---|
bool |
bool |
bool |
int8/16/32/64 |
int8_t 等 |
int |
float32/float64 |
float/double |
float |
string |
std::string |
str |
string[] |
std::vector<std::string> |
list |
builtin_interfaces/Time |
builtin_interfaces::msg::Time |
Time |
geometry_msgs/Pose |
geometry_msgs::msg::Pose |
Pose |
规则:字段名小写下划线;数组用 [] 后缀;嵌套类型写 包名/类型名。
4.4 定义 .srv 文件
在 my_interfaces/srv/ 创建 SetSpeed.srv(--- 分隔请求与响应):
# 请求部分float64 target_speedbool is_emergency_stop
---# 响应部分bool successstring message4.5 修改 CMakeLists.txt 启用接口生成
接口包的 CMakeLists.txt 需要添加 rosidl 相关配置:
find_package(rosidl_default_generators REQUIRED)find_package(builtin_interfaces REQUIRED)
# 声明接口文件(msg 与 srv 分开列)set(msg_files "msg/RobotStatus.msg")set(srv_files "srv/SetSpeed.srv")
# 生成接口(核心命令)rosidl_generate_interfaces(${PROJECT_NAME} ${msg_files} ${srv_files} DEPENDENCIES builtin_interfaces)
# 接口包不需要普通可执行文件,直接 ament_packageament_package()package.xml 也要加依赖:
<depend>rosidl_default_generators</depend><depend>builtin_interfaces</depend><member_of_group>rosidl_interface_packages</member_of_group>4.6 在代码中使用自定义消息
C++:
#include "my_interfaces/msg/robot_status.hpp"#include "my_interfaces/srv/set_speed.hpp"
// 使用消息my_interfaces::msg::RobotStatus status;status.robot_name = "turtlebot";status.battery_level = 0.85f;Python:
from my_interfaces.msg import RobotStatusfrom my_interfaces.srv import SetSpeed
status = RobotStatus()status.robot_name = "turtlebot"status.battery_level = 0.854.7 接口命令速查
# 查看系统所有可用接口类型ros2 interface list
# 查看某个消息/服务的定义ros2 interface show my_interfaces/msg/RobotStatusros2 interface show my_interfaces/srv/SetSpeed
# 查看消息包含的字段ros2 interface proto my_interfaces/msg/RobotStatus五、两种包类型全面对比
| 维度 | ament_python | ament_cmake |
|---|---|---|
| 构建配置 | setup.py |
CMakeLists.txt |
| 语言 | Python | C/C++ |
| 构建速度 | 快(复制) | 慢(编译) |
| 改代码生效 | --symlink-install 后免构建 |
必须重新 build |
| 性能 | 较低 | 高 |
| 适合场景 | 原型、脚本、快速迭代 | 性能敏感、实时控制、复杂算法 |
| 接口包支持 | 一般不用于定义接口 | 必须(rosidl 依赖 CMake) |
| 单元测试 | pytest | gtest |
| 依赖声明 | 同一 package.xml | 同一 package.xml |
关键结论:
- 接口包只能用 ament_cmake 定义(rosidl 生成器依赖 CMake),Python 包可以消费接口但不能定义。
- 一个工作空间可以混合两种包,colcon 自动识别各自构建方式。
- 代码要跨语言复用时,接口定义放独立接口包,业务实现再分语言包。
六、实战:接口包 + 双语言消费者
6.1 完整演练
# 1. 创建接口包cd ~/ros2_ws/srcros2 pkg create my_interfaces --build-type ament_cmake \ --license Apache-2.0 --maintainer-name you --maintainer-email you@example.com --description "Custom interfaces"
# 2. 写入 msg/RobotStatus.msg(见 4.3)# 3. 写入 srv/SetSpeed.srv(见 4.4)# 4. 按 4.5 修改 CMakeLists.txt 与 package.xml
# 5. 构建接口包cd ~/ros2_wscolcon build --packages-select my_interfacessource install/setup.bash
# 6. 验证接口可用ros2 interface show my_interfaces/msg/RobotStatus6.2 用命令行发一个自定义话题(无代码验证)
# 发布自定义消息(需要提供完整字段值)ros2 topic pub /robot_status my_interfaces/msg/RobotStatus \ "{robot_name: 'turtle', battery_level: 0.9, status_code: 1, stamp: {sec: 0, nanosec: 0}}" --rate 1
# 另开终端订阅查看ros2 topic echo /robot_status如果终端能正常收发你的自定义消息,说明接口包定义、生成、环境加载全部正确——这是最快的“接口自检”方式。
6.3 常见接口错误排查
| 现象 | 原因 | 解决 |
|---|---|---|
ros2 interface show 找不到 |
接口包没构建或没 source | colcon build + source install/setup.bash |
编译报 No such file or directory |
include 路径错 | C++ 用 包名/msg/文件名.hpp 全路径 |
运行时 ImportError: No module named 'my_interfaces' |
Python 环境没加载 | 确认 source 了 install 目录 |
| 消息字段名对不上 | .msg 命名规范 | 字段一律小写下划线,重建接口包 |
七、总结
本篇文章拆解了 ROS2 包的完整结构:package.xml 的五种依赖声明、Python 包 setup.py 的四个必须配置、CMake 包 CMakeLists.txt 的三个易错点、自定义接口(.msg/.srv)的定义与生成流程,以及两种包类型的选型对比。
关键要点回顾:
package.xml是包的必要身份证,依赖不确定时统一用<depend>。- Python 包四必须:
name、packages、data_files(ament 索引+package.xml)、entry_points。 - CMake 包三易错:忘
install(TARGETS)、忘ament_target_dependencies、忘find_package。 - 自定义接口用独立接口包(ament_cmake + format 3 +
rosidl_generate_interfaces),支持跨语言消费。 .msg/.srv字段小写下划线,---分隔服务请求响应。- 接口自检最快方式:
ros2 interface show+ros2 topic pub/echo。
下一篇预告
下一篇正式进入 ROS2 通信核心:话题 Topic 通信(上)——发布者与订阅者。我们将深入话题机制(Publish/Subscribe 模型、去中心化发现、QoS 匹配),用 Python 与 C++ 各写一个发布者/订阅者对,理解 create_publisher、create_subscription 与回调机制,并掌握 ros2 topic 全套调试命令。
ROS2系列教程:package结构(CMake与Python两种包)
作者:Wang Shijie
本文链接:https://wsj-blog.pages.dev/posts/ros2-xi-lie-jiao-cheng-package-jie-gou-cmake-yu-python/
本文采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。
留言评论
评论资源加载失败,请重试。