文章目录
-
- [ROS2 工作空间与功能包完全指南](#ROS2 工作空间与功能包完全指南)
-
- [一、ROS2 工作空间(Workspace)](#一、ROS2 工作空间(Workspace))
-
- [1. 什么是工作空间?](#1. 什么是工作空间?)
- [2. 工作空间的目录结构](#2. 工作空间的目录结构)
- [3. 工作空间的类型:Overlay 与 Underlay](#3. 工作空间的类型:Overlay 与 Underlay)
- [4. 创建工作空间](#4. 创建工作空间)
- [5. 编译工作空间](#5. 编译工作空间)
- [二、ROS2 功能包(Package)](#二、ROS2 功能包(Package))
-
- [1. 什么是功能包?](#1. 什么是功能包?)
- [2. 功能包的最小结构](#2. 功能包的最小结构)
- [3. 创建功能包](#3. 创建功能包)
- [4. 功能包的依赖管理](#4. 功能包的依赖管理)
- [5. 编译功能包](#5. 编译功能包)
- 三、功能测试python代码示例:发布者与订阅者
- [ROS2 工作空间与功能包(C++ 示例)](#ROS2 工作空间与功能包(C++ 示例))
ROS2 工作空间与功能包完全指南
在 ROS2 中,工作空间(workspace) 和 功能包(package) 是组织和管理代码的核心概念。理解它们对于开发 ROS2 应用程序至关重要。本文将全面介绍这两个概念,并提供一个完整的 Python 发布-订阅示例,每行代码都附带详细注释。
一、ROS2 工作空间(Workspace)
1. 什么是工作空间?
工作空间是一个包含 ROS2 功能包的目录,它提供了一种结构化的方式来组织、构建和运行多个相互关联的功能包。通过工作空间,你可以将自定义的代码与 ROS2 基础环境分离,方便管理和复用。
2. 工作空间的目录结构
一个典型的 ROS2 工作空间包含以下子目录:
workspace_name/
├── src # 存放所有功能包的源代码
├── build # 存放编译过程中的中间文件(colcon 自动生成)
├── install # 存放编译后的可执行文件、库、配置文件等(colcon 自动生成)
└── log # 存放编译日志(colcon 自动生成)
- src:你手动创建并放置自定义功能包的地方。
- build 、install 、log 由构建工具
colcon自动生成,通常不需要手动修改。
3. 工作空间的类型:Overlay 与 Underlay
- Underlay :基础的 ROS2 安装环境(如系统安装的
/opt/ros/humble)。每个新工作空间都可以叠加在现有环境之上。 - Overlay :你正在开发的本地工作空间,它依赖于 underlay 提供的依赖,但可以覆盖或扩展其中的功能包。当你
source一个工作空间的install/setup.bash后,该工作空间就成为了当前终端的 overlay。
4. 创建工作空间
使用以下命令创建一个新的工作空间(例如 ros2_ws):
bash
mkdir -p ~/ros2_ws/src # 创建工作空间根目录和 src 文件夹
cd ~/ros2_ws # 进入工作空间
此时工作空间还是空的,接下来我们需要在 src 文件夹中创建功能包。
5. 编译工作空间
ROS2 的官方构建工具是 colcon。在工作空间根目录下运行:
bash
colcon build
常用选项:
--packages-select <package_name>:仅编译指定的功能包。--symlink-install:使用符号链接安装,修改 Python 脚本后无需重新编译。--cmake-args -DCMAKE_BUILD_TYPE=Release:传递参数给 CMake。
编译完成后,需要 source 安装文件才能使用该工作空间:
bash
source install/setup.bash
(可以将其添加到 ~/.bashrc 中自动加载)
二、ROS2 功能包(Package)
1. 什么是功能包?
功能包是 ROS2 中代码组织的最小单元。它包含节点、库、配置文件、启动文件等,并且必须包含描述包信息和依赖关系的 package.xml 文件以及一个构建文件(CMakeLists.txt 用于 C++,setup.py 或 setup.cfg 用于 Python)。
2. 功能包的最小结构
以 Python 功能包为例:
my_package/
├── package.xml # 包清单文件(必须)
├── setup.py # Python 包的安装脚本(必须)
├── setup.cfg # 配置文件(通常用于声明 entry points)
├── resource/ # 标记文件夹(通常包含一个空文件 my_package)
├── test/ # 测试代码文件夹(可选)
└── my_package/ # 与包同名的 Python 模块,存放源代码
└── __init__.py
- package.xml:包含包名、版本、作者、许可证以及依赖项。
- setup.py:用于安装 Python 包,定义入口点(entry points)以便将脚本作为节点运行。
- setup.cfg:通常告诉 setuptools 如何安装脚本。
- resource:标记该文件夹为 ROS2 包(通常包含一个与包名相同的空文件)。
- my_package:实际的 Python 模块,放置节点代码。
对于 C++ 包,则使用 CMakeLists.txt 和 include/、src/ 等目录。
3. 创建功能包
使用 ros2 pkg create 命令创建功能包。例如创建一个 Python 包:
bash
cd ~/ros2_ws/src
ros2 pkg create my_package --build-type ament_python --dependencies rclpy std_msgs
--build-type:指定构建类型,ament_python用于 Python,ament_cmake用于 C++。--dependencies:列出该包依赖的其他 ROS2 包(如rclpy、std_msgs)。这些依赖会被自动添加到package.xml中。- 创建一个C++包
cpp
cd ~/ros2_ws/src
ros2 pkg create cpp_pubsub --build-type ament_cmake --dependencies rclcpp std_msgs
cpp_pubsub/
├── CMakeLists.txt # 构建配置文件
├── package.xml # 包清单文件
├── include/ # 存放头文件(可选)
│ └── cpp_pubsub/
└── src/ # 存放源文件
4. 功能包的依赖管理
在 package.xml 中,依赖通过以下标签声明:
<depend>:构建和运行时都需要。<build_depend>:仅构建时需要。<build_export_depend>:导出到依赖此包的其他包所需的依赖。<exec_depend>:仅运行时需要。<test_depend>:仅测试时需要。
例如,package.xml 中的依赖部分可能如下:
xml
<depend>rclpy</depend>
<depend>std_msgs</depend>
5. 编译功能包
在工作空间根目录使用 colcon build,可以指定只编译该包:
bash
colcon build --packages-select my_package
三、功能测试python代码示例:发布者与订阅者
下面我们创建一个简单的发布者-订阅者示例,使用 Python 编写,并逐行注释。该示例包含两个节点:一个发布字符串消息,一个订阅并打印消息。
1. 创建功能包
首先,确保你已经创建了工作空间(如 ~/ros2_ws),然后在 src 目录下创建包:
bash
cd ~/ros2_ws/src
ros2 pkg create py_pubsub --build-type ament_python --dependencies rclpy std_msgs
这条命令会生成一个名为 py_pubsub 的 Python 包,并声明依赖 rclpy(ROS2 Python 客户端库)和 std_msgs(标准消息类型)。
2. 编写发布者节点
在 py_pubsub/py_pubsub 目录下创建 publisher.py 文件:
bash
cd ~/ros2_ws/src/py_pubsub/py_pubsub
touch publisher.py
chmod +x publisher.py # 赋予执行权限(可选)
编辑 publisher.py,内容如下:
python
#!/usr/bin/env python3
# 指定解释器为 Python3,使得可以直接执行该脚本
import rclpy # 导入 ROS2 Python 客户端库
from rclpy.node import Node # 导入 Node 类,所有 ROS2 节点都继承自它
from std_msgs.msg import String # 导入标准字符串消息类型
class MinimalPublisher(Node):
"""
自定义的发布者节点类,继承自 Node。
"""
def __init__(self):
# 调用父类 Node 的构造函数,节点名称为 'minimal_publisher'
super().__init__('minimal_publisher')
# 创建一个发布者,发布到 'topic' 话题,消息类型为 String,队列长度为 10
self.publisher_ = self.create_publisher(String, 'topic', 10)
# 创建一个定时器,每隔 0.5 秒调用一次 timer_callback 函数
# 参数:定时器周期(秒),回调函数
self.timer = self.create_timer(0.5, self.timer_callback)
# 初始化计数器
self.i = 0
def timer_callback(self):
"""
定时器回调函数:创建消息并发布。
"""
# 创建 String 类型的消息对象
msg = String()
# 设置消息内容,包含计数器的当前值
msg.data = 'Hello World: %d' % self.i
# 调用发布者的 publish 方法发布消息
self.publisher_.publish(msg)
# 使用节点的日志系统打印信息(相当于 ROS1 的 ROS_INFO)
self.get_logger().info('Publishing: "%s"' % msg.data)
# 计数器递增
self.i += 1
def main(args=None):
"""
主函数:初始化节点,进入事件循环,最后清理。
"""
# 初始化 ROS2 客户端库
rclpy.init(args=args)
# 创建 MinimalPublisher 类的实例
minimal_publisher = MinimalPublisher()
# 进入事件循环,等待回调函数被触发(此处会阻塞,直到节点被关闭)
rclpy.spin(minimal_publisher)
# 当 spin 结束后(例如按 Ctrl+C),销毁节点(可选,但推荐)
minimal_publisher.destroy_node()
# 关闭 ROS2 客户端库
rclpy.shutdown()
if __name__ == '__main__':
main()
3. 编写订阅者节点
同样在 py_pubsub/py_pubsub 目录下创建 subscriber.py 文件:
bash
touch subscriber.py
chmod +x subscriber.py
编辑 subscriber.py:
python
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
class MinimalSubscriber(Node):
"""
自定义的订阅者节点类,继承自 Node。
"""
def __init__(self):
# 节点名称为 'minimal_subscriber'
super().__init__('minimal_subscriber')
# 创建一个订阅者,订阅 'topic' 话题,消息类型为 String,队列长度为 10
# 当收到消息时,调用 listener_callback 函数
self.subscription = self.create_subscription(
String,
'topic',
self.listener_callback,
10)
# 为了防止 "unused variable" 警告,可以将 subscription 保存为成员变量(如上)
# 但实际上 self.subscription 被使用了,所以没问题
def listener_callback(self, msg):
"""
消息回调函数:收到消息时打印内容。
"""
# 使用节点的日志系统打印收到的消息
self.get_logger().info('I heard: "%s"' % msg.data)
def main(args=None):
rclpy.init(args=args)
minimal_subscriber = MinimalSubscriber()
rclpy.spin(minimal_subscriber) # 持续等待消息
minimal_subscriber.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
4. 配置 setup.py
为了使 ros2 run 能够找到我们的节点,需要在 setup.py 的 entry_points 中添加控制台脚本入口。编辑 py_pubsub/setup.py:
python
from setuptools import setup
import os
from glob import glob
package_name = 'py_pubsub'
setup(
name=package_name,
version='0.0.0',
packages=[package_name],
data_files=[
# 安装 package.xml 到 share 目录,以便 ROS2 发现该包
(os.path.join('share', package_name), ['package.xml']),
# 如果有启动文件或其他资源,也可以在这里添加
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='your_name',
maintainer_email='your_email@example.com',
description='Python publisher and subscriber example',
license='Apache License 2.0', # 或其他许可证
tests_require=['pytest'],
entry_points={
'console_scripts': [
# 格式: "命令名 = 包名.模块名:主函数名"
'publisher = py_pubsub.publisher:main',
'subscriber = py_pubsub.subscriber:main',
],
},
)
5. 编译与运行
-
编译工作空间:
bashcd ~/ros2_ws colcon build --packages-select py_pubsub -
加载工作空间环境(如果尚未加载):
bashsource install/setup.bash -
运行发布者 :
打开终端1:
bashsource ~/ros2_ws/install/setup.bash ros2 run py_pubsub publisher你将看到每隔 0.5 秒打印一条发布消息。
-
运行订阅者 :
打开终端2:
bashsource ~/ros2_ws/install/setup.bash ros2 run py_pubsub subscriber订阅者会收到发布者发送的消息并打印。
6. 查看节点和话题
- 使用
ros2 node list查看运行中的节点。 - 使用
ros2 topic list查看可用话题。 - 使用
ros2 topic echo /topic直接查看话题上的消息。
ROS2 工作空间与功能包(C++ 示例)
在前面的回答中,我们已经详细介绍了 ROS2 工作空间和功能包的概念。本文将重点聚焦于 C++ 功能包的创建与测试代码,提供一个完整的发布者-订阅者示例,并逐行注释代码。通过这个示例,你将学会如何组织 C++ 节点、配置构建文件,并成功运行。
一、准备工作
确保你已经安装了 ROS2(如 Humble、Iron 或 Rolling),并创建了一个工作空间(例如 ros2_ws):
bash
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws
二、创建 C++ 功能包
使用 ros2 pkg create 命令创建一个 C++ 包,并指定依赖项 rclcpp(ROS2 C++ 客户端库)和 std_msgs(标准消息类型):
bash
cd ~/ros2_ws/src
ros2 pkg create cpp_pubsub --build-type ament_cmake --dependencies rclcpp std_msgs
--build-type ament_cmake:指定构建类型为 CMake(C++ 包的标准)。--dependencies:自动将依赖添加到package.xml和CMakeLists.txt中。
生成的目录结构如下:
cpp_pubsub/
├── CMakeLists.txt # 构建配置文件
├── package.xml # 包清单文件
├── include/ # 存放头文件(可选)
│ └── cpp_pubsub/
└── src/ # 存放源文件
三、编写发布者节点
在 src 目录下创建 publisher.cpp 文件:
bash
cd ~/ros2_ws/src/cpp_pubsub/src
touch publisher.cpp
编辑 publisher.cpp,内容如下(每一行都有详细注释):
cpp
// 包含必要的头文件
#include <chrono> // 时间相关,用于定时器周期
#include <functional> // 用于 std::bind
#include <memory> // 用于 std::make_shared
#include <string> // 字符串处理
#include "rclcpp/rclcpp.hpp" // ROS2 C++ 客户端库
#include "std_msgs/msg/string.hpp" // 标准字符串消息类型
using namespace std::chrono_literals; // 允许使用 500ms 这样的字面量
/* 发布者节点类,继承自 rclcpp::Node */
class MinimalPublisher : public rclcpp::Node
{
public:
MinimalPublisher()
: Node("minimal_publisher"), count_(0) // 节点名称,并初始化计数器
{
// 创建发布者:话题名称 "topic",消息类型 std_msgs::msg::String,队列大小 10
publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
// 创建定时器:每隔 500ms 调用一次 timer_callback 函数
timer_ = this->create_wall_timer(
500ms, std::bind(&MinimalPublisher::timer_callback, this));
}
private:
// 定时器回调函数
void timer_callback()
{
// 创建一个 String 消息对象
auto message = std_msgs::msg::String();
message.data = "Hello, world! " + std::to_string(count_++);
// 使用 RCLCPP_INFO 宏打印日志(类似 ROS1 的 ROS_INFO)
RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
// 发布消息
publisher_->publish(message);
}
// 成员变量
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_; // 发布者智能指针
rclcpp::TimerBase::SharedPtr timer_; // 定时器智能指针
size_t count_; // 消息计数器
};
/* 主函数 */
int main(int argc, char * argv[])
{
// 初始化 ROS2
rclcpp::init(argc, argv);
// 创建节点实例并进入事件循环(spin 会一直阻塞,直到节点关闭)
rclcpp::spin(std::make_shared<MinimalPublisher>());
// 关闭 ROS2
rclcpp::shutdown();
return 0;
}
四、编写订阅者节点
在 src 目录下创建 subscriber.cpp 文件:
bash
touch subscriber.cpp
编辑 subscriber.cpp,内容如下:
cpp
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using std::placeholders::_1; // 用于 std::bind 占位符
/* 订阅者节点类 */
class MinimalSubscriber : public rclcpp::Node
{
public:
MinimalSubscriber()
: Node("minimal_subscriber")
{
// 创建订阅者:话题 "topic",消息类型 std_msgs::msg::String,队列大小 10
// 当收到消息时,调用 topic_callback 函数
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
}
private:
// 消息回调函数
void topic_callback(const std_msgs::msg::String::SharedPtr msg) const
{
// 打印接收到的消息
RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str());
}
// 成员变量:订阅者智能指针
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalSubscriber>());
rclcpp::shutdown();
return 0;
}
五、修改构建文件
C++ 包使用 CMakeLists.txt 来定义如何编译可执行文件。打开 cpp_pubsub/CMakeLists.txt,并进行如下修改:
cmake
cmake_minimum_required(VERSION 3.8)
project(cpp_pubsub)
# 默认使用 C++17 标准(ROS2 要求)
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) # 查找 rclcpp
find_package(std_msgs REQUIRED) # 查找 std_msgs
# 编译发布者节点可执行文件
add_executable(publisher src/publisher.cpp)
# 链接需要的库
ament_target_dependencies(publisher rclcpp std_msgs)
# 编译订阅者节点可执行文件
add_executable(subscriber src/subscriber.cpp)
ament_target_dependencies(subscriber rclcpp std_msgs)
# 安装可执行文件到 lib 目录,使得 ros2 run 可以找到
install(TARGETS
publisher
subscriber
DESTINATION lib/${PROJECT_NAME})
# 如果存在 launch 文件或其他资源,也可以在这里安装
# ament 包相关设置
ament_package()
注意 :ament_target_dependencies 是 ROS2 提供的便捷函数,它会自动处理头文件路径和依赖库链接。
package.xml 文件在创建包时已自动添加了依赖,检查一下确保包含以下内容:
xml
<depend>rclcpp</depend>
<depend>std_msgs</depend>
六、编译与运行
-
编译工作空间:
bashcd ~/ros2_ws colcon build --packages-select cpp_pubsub -
加载工作空间环境 (每次新终端都需要执行,或添加到
~/.bashrc):bashsource ~/ros2_ws/install/setup.bash -
运行发布者节点 :
打开终端1:
bashros2 run cpp_pubsub publisher你将看到每 0.5 秒输出一条发布日志。
-
运行订阅者节点 :
打开终端2:
bashros2 run cpp_pubsub subscriber订阅者会打印接收到的消息。
七、验证与调试
- 使用
ros2 node list查看正在运行的节点,应看到/minimal_publisher和/minimal_subscriber。 - 使用
ros2 topic list查看话题,应看到/topic。 - 使用
ros2 topic echo /topic直接查看话题上的消息流。 - 如果需要重新编译,可以在工作空间根目录再次执行
colcon build,或者只编译修改过的包:colcon build --packages-select cpp_pubsub。