ROS2基础使用(工作空间构建、编译和运行)
ROS2基础使用(工作空间构建、编译和运行)
本文档详细介绍了ROS2工作空间的创建、编译和运行方法,涵盖C++和Python两种开发方式。
主要内容包括工作空间的标准目录结构、关键配置文件(CMakeLists.txt、package.xml等)的编写规范、节点代码实现示例,以及使用colcon工具进行编译和运行的相关命令。本文还提供了一个自动化脚本,可快速生成ROS2包的基础结构,适合开发者快速上手ROS2项目开发。
操作系统:Ubuntu 22.04
ROS2版本:Humble
语言:C++/Python 3.12
目录
1. 创建ROS2工作空间
典型的C++和Python工作空间结构如下:
# C++工作空间结构
cpp_package/
├── CMakeLists.txt
├── package.xml
├── src/ # C++源文件目录
│ └── cpp_node.cpp
├── include/ # C++头文件目录
│ └── cpp_package/
│ └── my_header.hpp
└── launch/ # 启动文件目录
└── cpp_node.launch.py
# Python工作空间结构
py_package/
├── package.xml
├── setup.py
├── setup.cfg
├── py_package/
│ ├── __init__.py
│ └── python_node.py
└── launch/
└── python_node.launch.py
2. 关键文件内容
2.1 CMakeLists.txt
使用C++时,需要在CMakeLists.txt文件中,定义编译依赖关系,以及编译目标。
其主要包含以下内容:
project(cpp_package)为设置项目名称为cpp_package;find_package用于查找和加载预定义的包及其依赖项。ament_cmake依赖是ROS 2的CMake工具链的一部分,用于提供ROS2特有的CMake功能;rclcpp依赖是ROS2的C++接口,用于与ROS2进行通信;此处可添加其他依赖项如std_msgs、sensor_msgs以及第三方依赖项如Eigen、Boost等。add_executable()用于构建一个可执行文件,参数为可执行文件的名称(此处为创建一个名为cpp_node的可执行文件)和源文件的列表(此处为添加src/cpp_node.cpp文件,可添加其他源文件)。如果有多个节点,每个节点都需要添加一个add_executable(),且每个节点均会生成一个可执行文件;target_include_directories()用于指定目标(如库或可执行文件,此处为名为cpp_node的可执行文件)的头文件搜索路径,其中$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>为在构建过程中,CMake会添加当前源目录下的include目录到头文件搜索路径中,$<INSTALL_INTERFACE:include>为在安装过程中,CMake会添加安装目录下的include目录到头文件搜索路径中。如果有多个节点,每个节点均需要添加一个target_include_directories();ament_target_dependencies()用于指定目标(如库或可执行文件,此处为名为cpp_node的可执行文件)的依赖项,此处为依赖rclcpp和std_msgs两个库,此处可添加其他依赖项如sensor_msgs以及第三方依赖项如Eigen、Boost等。如果有多个节点,每个节点的依赖项都使用ament_target_dependencies()添加。
# 设置Cmake版本与项目名称
cmake_minimum_required(VERSION 3.5)
project(cpp_package)
# 设置C++标准
set(CMAKE_CXX_STANDARD 17)
# 查找依赖
find_package(ament_cmake REQUIRED) # 必须
find_package(rclcpp REQUIRED) # 必须
find_package(std_msgs REQUIRED) # 可选(依赖标准消息类型)
# 通过源文件构建目标
add_executable(cpp_node # 添加所有源文件
src/cpp_node.cpp
)
# 为目标添加头文件位置
target_include_directories(cpp_node PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>)
# 为目标添加依赖关系
ament_target_dependencies(cpp_node
rclcpp
std_msgs
)
# 安装目标到指定的目录
install(TARGETS cpp_node
DESTINATION lib/${PROJECT_NAME})
# 安装头文件目录
install(DIRECTORY include/ DESTINATION include)
# 安装launch和config目录
install(DIRECTORY launch config
DESTINATION share/${PROJECT_NAME})
ament_package()
Tips: 可通过
set(NODE_NAME cpp_node)设置目标的名称cpp_node为一个名为NODE_NAME的变量,当需要引用时,可用${NODE_NAME},如add_executable(${NODE_NAME} src/cpp_node.cpp)。
2.2 package.xml
package.xml文件定义了package的属性。(例如:包名,版本,描述,作者,依赖等等),相当于一个包的自我描述,主要包含以下内容:
<name>,<version>,<description>:包的名称、版本、描述。<maintainer>,<license>:包的维护者和许可信息。<buildtool_depend>,<depend>,<test_depend>:依赖的构建工具、运行库、测试库。<export>:导出标记。
下面分别是C++版本的package.xml文件,Python版本的文件与C++版本差别不大,主要在于<buildtool_depend>和<build_type>的不同。
<?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>cpp_package</name>
<version>0.1.0</version>
<description>A C++ ROS2 package example</description>
<!-- 维护者信息 -->
<maintainer email="your.email@example.com">Your Name</maintainer>
<!-- 许可证 -->
<license>Apache License 2.0</license>
<!-- 构建工具依赖 -->
<!-- Cmake 构建工具 -->
<buildtool_depend>ament_cmake</buildtool_depend>
<!-- Python 构建工具 -->
<!-- <buildtool_depend>ament_python</buildtool_depend> -->
<!-- 构建和运行时依赖 -->
<depend>rclcpp</depend>
<depend>std_msgs</depend>
<!-- 可选:测试依赖 -->
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<!-- 导出标记 -->
<export>
<!-- Cmake 导出工具 -->
<build_type>ament_cmake</build_type>
<!-- Python 导出工具 -->
<!-- <build_type>ament_python</build_type> -->
</export>
</package>
2.3 cpp_node.launch.py
launch.py是ROS2的启动文件,用于配置和启动一个或多个ROS节点。针对启动单个名为cpp_node的节点可参考下列示例:
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
Node(
package='cpp_package',
executable='cpp_node',
name='cpp_node',
output='screen'
)
])
2.4 cpp_node.cpp
#include "rclcpp/rclcpp.hpp"
class CppNode : public rclcpp::Node {
public:
CppNode() : Node("cpp_node") {
// 创建一个定时器
timer_ = create_wall_timer(
std::chrono::seconds(1),
[this]() { this->timer_callback(); });
}
private:
// 定时器回调函数
void timer_callback() {
RCLCPP_INFO(get_logger(), "Hello from C++ node!");
}
rclcpp::TimerBase::SharedPtr timer_;
};
// 主函数
int main(int argc, char * argv[]) {
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<CppNode>());
rclcpp::shutdown();
return 0;
}
2.5 setup.py
setup.py是 Python 包的构建配置文件,其主要包括:
name,version,packages: 包名、版本号、包含的 Python 包data_files: 包含的静态文件maintainer,maintainer_email,description,license: 包作者及信息、描述、许可证entry_points: 包的入口点,即 Python 模块
from setuptools import setup
package_name = 'py_package'
setup(
name=package_name,
version='0.1.0',
packages=[package_name],
data_files=[
('share/' + package_name, ['package.xml']),
('share/' + package_name + '/launch', ['launch/py_launch.launch.py']),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='your_name',
maintainer_email='your_email@example.com',
description='Python ROS2 package',
license='Apache License 2.0',
tests_require=['pytest'],
entry_points={
'console_scripts': [
'py_node = py_package.my_python_node:main',
],
},
)
2.6 setup.cfg
setup.cfg主要用来配置Python包的安装信息,如安装路径等。下面实例中的两项分别表示开发模式安装位置和安装模式安装位置。
[develop]
script_dir=$base/lib/py_package
[install]
install_scripts=$base/lib/py_package
2.7 python_node.py
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
class PythonNode(Node):
def __init__(self):
super().__init__('python_node')
self.timer = self.create_timer(1.0, self.timer_callback)
def timer_callback(self):
self.get_logger().info('Hello from Python node!')
def main(args=None):
rclpy.init(args=args)
node = PythonNode()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
2.8 一键创建脚本
使用下面脚本可一键创建C++或者Python的包示例,将该脚本保存为 create_ros2_pkg.sh ,并执行 chmod +x create_ros2_pkg.sh 。
对于C++包结构,请使用:
./create_ros2_pkg.sh my_pkg cpp node1 node2
对于Python包结构,请使用:
./create_ros2_pkg.sh my_pkg python node1 node2
执行后将在当前目录下创建一个名为 my_pkg 的目录,并在其下创建 CMakeLists.txt 、 package.xml 、 launch/my_pkg.launch.py 、 src/ 目录、 include/my_pkg 目录以及相关文件。
点击查看代码
#!/bin/bash
# Function to create C++ package structure
create_cpp_package() {
local pkg_name=$1
shift
local nodes=("$@")
# Create directories
mkdir -p ${pkg_name}/src
mkdir -p ${pkg_name}/include/${pkg_name}
mkdir -p ${pkg_name}/launch
# Create CMakeLists.txt
cat > ${pkg_name}/CMakeLists.txt <<EOF
cmake_minimum_required(VERSION 3.5)
project(${pkg_name})
# Default to C++17
if(NOT CMAKE_CXX_STANDARD)
set(CMAKE_CXX_STANDARD 17)
endif()
# Find dependencies
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
EOF
# Add nodes to CMakeLists.txt
for node in "${nodes[@]}"; do
cat >> ${pkg_name}/CMakeLists.txt <<EOF
# ${node} executable
add_executable(${node} src/${node}.cpp)
target_include_directories(${node} PUBLIC
\$<BUILD_INTERFACE:\${CMAKE_CURRENT_SOURCE_DIR}/include>
\$<INSTALL_INTERFACE:include>)
ament_target_dependencies(${node} rclcpp std_msgs)
EOF
done
# Add installation to CMakeLists.txt
cat >> ${pkg_name}/CMakeLists.txt <<EOF
# Install targets
install(TARGETS ${nodes[@]}
DESTINATION lib/\${PROJECT_NAME})
# Install include directory
install(DIRECTORY include/ DESTINATION include)
# Install launch files
install(DIRECTORY launch/ DESTINATION share/\${PROJECT_NAME}/launch)
ament_package()
EOF
# Create package.xml
cat > ${pkg_name}/package.xml <<EOF
<?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>${pkg_name}</name>
<version>0.0.0</version>
<description>${pkg_name} package</description>
<maintainer email="user@email.com">Your Name</maintainer>
<license>Apache License 2.0</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<depend>rclcpp</depend>
<depend>std_msgs</depend>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>
EOF
# Create node source files and launch file
for node in "${nodes[@]}"; do
# Create .cpp file
cat > ${pkg_name}/src/${node}.cpp <<EOF
#include "rclcpp/rclcpp.hpp"
class ${node^} : public rclcpp::Node {
public:
${node^}() : Node("${node}") {
timer_ = create_wall_timer(
std::chrono::seconds(1),
[this]() { this->timer_callback(); });
}
private:
void timer_callback() {
RCLCPP_INFO(get_logger(), "Hello from ${node}!");
}
rclcpp::TimerBase::SharedPtr timer_;
};
int main(int argc, char * argv[]) {
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<${node^}>());
rclcpp::shutdown();
return 0;
}
EOF
# Create header file
cat > ${pkg_name}/include/${pkg_name}/${node}.hpp <<EOF
#ifndef ${pkg_name^^}_${node^^}_HPP
#define ${pkg_name^^}_${node^^}_HPP
// Add your header content here
#endif // ${pkg_name^^}_${node^^}_HPP
EOF
done
# Create launch file
cat > ${pkg_name}/launch/${pkg_name}.launch.py <<EOF
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
EOF
for node in "${nodes[@]}"; do
cat >> ${pkg_name}/launch/${pkg_name}.launch.py <<EOF
Node(
package='${pkg_name}',
executable='${node}',
name='${node}',
output='screen'
),
EOF
done
cat >> ${pkg_name}/launch/${pkg_name}.launch.py <<EOF
])
EOF
echo "Created C++ package '${pkg_name}' with nodes: ${nodes[@]}"
}
# Function to create Python package structure
create_python_package() {
local pkg_name=$1
shift
local nodes=("$@")
# Create directories
mkdir -p ${pkg_name}/${pkg_name}
mkdir -p ${pkg_name}/launch
# Create package.xml
cat > ${pkg_name}/package.xml <<EOF
<?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>${pkg_name}</name>
<version>0.0.0</version>
<description>${pkg_name} package</description>
<maintainer email="user@email.com">Your Name</maintainer>
<license>Apache License 2.0</license>
<buildtool_depend>ament_python</buildtool_depend>
<depend>rclpy</depend>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<export>
<build_type>ament_python</build_type>
</export>
</package>
EOF
# Create setup.py
cat > ${pkg_name}/setup.py <<EOF
from setuptools import setup
package_name = '${pkg_name}'
setup(
name=package_name,
version='0.0.0',
packages=[package_name],
data_files=[
('share/ament_index/resource_index/packages',
['resource/' + package_name]),
('share/' + package_name, ['package.xml']),
('share/' + package_name + '/launch', ['launch/${pkg_name}.launch.py']),
],
install_requires=['setuptools'],
zip_safe=True,
maintainer='Your Name',
maintainer_email='user@email.com',
description='${pkg_name} package',
license='Apache License 2.0',
tests_require=['pytest'],
entry_points={
'console_scripts': [
EOF
for node in "${nodes[@]}"; do
cat >> ${pkg_name}/setup.py <<EOF
'${node} = ${pkg_name}.${node}:main',
EOF
done
cat >> ${pkg_name}/setup.py <<EOF
],
},
)
EOF
# Create setup.cfg
cat > ${pkg_name}/setup.cfg <<EOF
[develop]
script_dir=\$base/lib/${pkg_name}
[install]
install_scripts=\$base/lib/${pkg_name}
EOF
# Create __init__.py
touch ${pkg_name}/${pkg_name}/__init__.py
# Create node files and launch file
for node in "${nodes[@]}"; do
# Create .py file
cat > ${pkg_name}/${pkg_name}/${node}.py <<EOF
#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
class ${node^}(Node):
def __init__(self):
super().__init__('${node}')
self.timer = self.create_timer(1.0, self.timer_callback)
def timer_callback(self):
self.get_logger().info('Hello from ${node}!')
def main(args=None):
rclpy.init(args=args)
node = ${node^}()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
EOF
chmod +x ${pkg_name}/${pkg_name}/${node}.py
done
# Create launch file
cat > ${pkg_name}/launch/${pkg_name}.launch.py <<EOF
from launch import LaunchDescription
from launch_ros.actions import Node
def generate_launch_description():
return LaunchDescription([
EOF
for node in "${nodes[@]}"; do
cat >> ${pkg_name}/launch/${pkg_name}.launch.py <<EOF
Node(
package='${pkg_name}',
executable='${node}',
name='${node}',
output='screen'
),
EOF
done
cat >> ${pkg_name}/launch/${pkg_name}.launch.py <<EOF
])
EOF
echo "Created Python package '${pkg_name}' with nodes: ${nodes[@]}"
}
# Main script
if [ "$#" -lt 3 ]; then
echo "Usage: $0 <package_name> <cpp|python> <node1> [node2 ...]"
exit 1
fi
pkg_name=$1
shift
pkg_type=$1
shift
nodes=("$@")
# Create workspace directory
case $pkg_type in
cpp)
create_cpp_package $pkg_name "${nodes[@]}"
;;
python)
create_python_package $pkg_name "${nodes[@]}"
;;
*)
echo "Invalid package type: $pkg_type (must be 'cpp' or 'python')"
exit 1
;;
esac
cd ..
echo "ROS2 workspace created in ${pkg_name}"
3. 编译和运行
3.1 编译
编译工作空间中所有包和节点:
colcon build
编译指定的包package_name:
colcon build --packages-select package_name
编译指定的节点node_name:
colcon build --cmake-target node_name
并行编译:
colcon build --parallel-workers 8
使用符号链接而非复制文件(加快开发迭代速度):
colcon build --symlink-install
指定编译类型(Debug/Release):
colcon build --cmake-args -DCMAKE_BUILD_TYPE=Debug
#colcon build --cmake-args -DCMAKE_BUILD_TYPE=Release
推荐使用:
colcon build --symlink-install --parallel-workers 8
colcon build --symlink-install --parallel-workers 8 --packages-select package_name
编译后必须初始化环境才能运行:
source install/setup.sh
编译会产生build、install和log三个目录,如需清理,执行:
rm -rf build install log
Tips: C++的包在修改完之后,必须重新编译才能生效,而Python的包只需要重新初始化环境即可。
3.2 运行
-
方案1
使用ros2 run直接运行名为package_name的包内名为node_name的节点:ros2 run package_name node_name -
方案2
使用ros2 launch运行名为package_name的包内名为simple.launch.py的启动文件:ros2 launch package_name simple.launch.py
3.3 ros2 相关命令
查看运行中的节点:
ros2 node list
查看节点发布的话题
ros2 topic list
浙公网安备 33010602011771号