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_msgssensor_msgs 以及第三方依赖项如 EigenBoost 等。
  • 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 的可执行文件)的依赖项,此处为依赖 rclcppstd_msgs 两个库,此处可添加其他依赖项如 sensor_msgs 以及第三方依赖项如 EigenBoost 等。如果有多个节点,每个节点的依赖项都使用 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.txtpackage.xmllaunch/my_pkg.launch.pysrc/ 目录、 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

编译会产生buildinstalllog三个目录,如需清理,执行:

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
posted @ 2025-07-08 15:31  TDC|唐朝板栗  阅读(1720)  评论(0)    收藏  举报