初识ROS2ROS 2Robot Operating System 2是一个面向机器人开发的开源软件框架可以理解为机器人软件的“操作系统”但它本身并不是一个真正的操作系统而是一套帮助开发机器人应用的工具、库和通信机制。在ROS2中提供了一套简便易用的进程间通信机制通过这套机制可以将机器人的各个模块间的通信变得标准化易用化。而且在ROS2强大的生态支持下很多常用包(Package)的安装和使用都变得非常简单开发者可以专注于算法的实现而不必过多关注底层通信细节。ROS2 的核心概念包括节点Node、话题Topic、服务Service、动作Action、消息Message、软件包Package和启动文件Launch。这些概念构成了ROS2的基础使得开发者能够高效地构建和管理机器人应用。不同部分的作用如下表所示概念 作用Node节点 一个独立运行的程序Topic话题 发布/订阅消息实现数据广播Service服务 请求-响应通信Action动作 用于执行耗时任务如导航Message消息 节点之间传输的数据格式Package软件包 管理代码和资源Launch 一键启动多个节点对于一个使用ROS2的项目其项目结构通常如下所示my_robot_ws/ # ROS 2 工作空间 (Workspace)│├── src/ # 源代码空间 (Source Space)│ │ # 存放所有 ROS 2 软件包 (Package)│ ││ └── my_robot_package/ # 自定义 ROS 2 软件包│ ││ ├── CMakeLists.txt # CMake 构建配置文件│ ├── package.xml # 软件包描述文件与依赖管理│ ││ ├── src/ # C 源文件目录│ │ └── *.cpp│ ││ └── include/ # 头文件目录│ └── my_robot_package/│ └── *.hpp│├── build/ # 构建空间 (Build Space)│ # 存放 CMake 编译过程产生的中间文件│├── install/ # 安装空间 (Install Space)│ # 存放编译安装后的可执行文件、库、配置文件│└── log/ # 日志空间 (Log Space)# 存放 colcon 构建过程日志当然上面的结构是用C编写的ROS2包的典型结构如果使用Python编写结构会有所不同但核心概念和工作空间的组织方式是类似的。下面我将一一解释这些概念并提供一些常用的ROS2命令。工作空间(Workspace)、软件包(Package)、节点(Node)在ROS2中工作空间是整个项目的根目录软件包是组织代码和资源的基本单位每个软件包可以包含一个或多个节点。节点是ROS2中最小的执行单元通常对应一个独立运行的程序。节点之间通过话题、服务和动作进行通信。比如在一个机器人项目中需要使用的硬件功能有摄像头(Camera)导航(Navigation)和机械臂(Arm)等功能而每个硬件又可以对应一个或多个节点来实现其功能如摄像头包中可能包含一个节点用于图像采集一个节点用于图像处理。则其相对应的工作空间结构可能如下my_robot_ws/ # 使用后缀_ws表示工作空间(即workspace的缩写)├── src/│ ├── camera_package/ # 摄像头软件包│ │ ├── camera_node.cpp # 摄像头节点│ │ └── image_processing_node.cpp # 图像处理节点│ ├── navigation_package/ # 导航软件包│ │ └── navigation_node.cpp # 导航节点│ └── arm_package/ # 机械臂软件包│ └── arm_control_node.cpp # 机械臂控制节点这个结构清晰地展示了工作空间、软件包和节点之间的关系。每个软件包可以包含多个节点每个节点可以独立运行并与其他节点进行通信。软件包的创建和管理在ROS2中软件包是组织代码和资源的基本单位。创建一个新的软件包通常使用ros2 pkg create命令。例如要创建一个名为robot_package的软件包可以使用以下命令ros2 pkg createpackage_name \ # 软件包名称–build-type build_type \ # 构建类型如 ament_cmake 或 ament_python–dependencies … \ # 依赖的其他软件包–maintainer-name \ # 维护者名称–maintainer-email \ # 维护者邮箱–description “” \ # 软件包描述–license \ # 许可证类型–node-name node_name # 节点名称一个简单的C软件包创建示例ros2 pkg create robot_package --build-type ament_cmake --dependencies rclcpp --license Apache-2.0创建一个名为robot_package的软件包使用ament_cmake构建类型并依赖rclcpp库并指定Apache-2.0许可证。当然若创建Python软件包则可以使用ament_python构建类型并依赖rclpy库。dependencies参数用于指明软件包所依赖的其他ROS2软件包这些依赖关系在创建包时会被记录在package.xml文件中以及会自动的在CMakeLists.txt中添加相应的依赖配置(find_package(依赖包))。创建完毕的软件包如下图所示image其中include目录用于存放头文件src目录用于存放源代码文件CMakeLists.txt和package.xml是软件包的配置文件。CMakeLists.txt用于定义如何构建软件包而package.xml用于描述软件包的元数据和依赖关系。其中package.xml文件的内容如下所示可以看见文件中将软件包的各类信息进行了详细的描述包括软件包名称、版本、描述、维护者信息、许可证类型以及依赖关系等。这个就相当于软件包的“身份证”在ROS2中是非常重要的文件。?xml version1.0? ?xml-model hrefhttp://download.ros.org/schema/package_format3.xsd schematypenshttp://www.w3.org/2001/XMLSchema? robot_package 0.0.0 TODO: Package description rose Apache-2.0buildtool_dependament_cmake/buildtool_dependrclcpptest_dependament_lint_auto/test_dependtest_dependament_lint_common/test_dependament_cmake 注意此后在编写软件包时如果需要添加新的依赖包则需要在package.xml中添加相应的依赖声明并在CMakeLists.txt中添加相应的find_package()配置。new_dependency如opencv节点的创建和运行在ROS2中节点是最小的执行单元通常对应一个独立运行的程序。每个节点可以独立运行并与其他节点进行通信。一个节点对应到一个可执行文件通常在src目录下编写源代码文件并在CMakeLists.txt中进行配置以生成可执行文件。在C中创建一个简单的节点示例#include “rclcpp/rclcpp.hpp”// rclcpp即ros2 client library for C是ROS2中C客户端库的核心头文件包含了创建节点、发布/订阅消息、服务调用等功能所需的类和函数。// 同理 rclpy是ROS2中Python客户端库的核心模块提供了类似的功能。class MyNode : public rclcpp::Node // 所有节点必须继承自rclcpp::Node类{public:MyNode() : Node(my_node) // 构造函数中调用父类构造函数并传入节点名称 { RCLCPP_INFO(this-get_logger(), Hello ROS2 Node!); RCLCPP_INFO(this-get_logger(), Node name: %s, this-get_name()); // 使用RCLCPP_INFO宏函数打印日志信息this-get_logger()获取节点的日志记录器来输出日志信息后面的参数可以是字符串也可以是格式化字符串类似于printf函数的用法。 }};int main(int argc, char **argv){// 初始化 ROS2rclcpp::init(argc, argv);// 创建节点的智能指针在ROS2中使用智能指针去管理各类对象是非常常用的能够有效避免内存泄漏和悬空指针等问题。 auto node std::make_sharedMyNode(); // 保持节点运行进入循环并等待事件的发生直到节点被关闭 // 如果学过Qt编程这个操作很像Qt中的事件循环机制Qt中使用app.exec()来启动事件循环而ROS2中使用rclcpp::spin(node)来启动节点的事件循环。 rclcpp::spin(node); // 关闭 ROS2 并释放资源 rclcpp::shutdown(); return 0;}在编写完节点代码后需要在CMakeLists.txt中添加相应的配置以生成可执行文件。在创建软件包时CMakeLists.txt文件中已经包含了基本的构建配置。cmake_minimum_required(VERSION 3.8)project(robot_package)if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES “Clang”)add_compile_options(-Wall -Wextra -Wpedantic) # 添加编译选项启用警告信息endif()find dependencies 添加依赖包find_package(ament_cmake REQUIRED)find_package(rclcpp REQUIRED)如果开启了测试才执行下面的代码if(BUILD_TESTING)find_package(ament_lint_auto REQUIRED)the following line skips the linter which checks for copyrightscomment the line when a copyright and license is added to all source filesset(ament_cmake_copyright_FOUND TRUE)the following line skips cpplint (only works in a git repo)comment the line when this package is in a git repo and whena copyright and license is added to all source filesset(ament_cmake_cpplint_FOUND TRUE)ament_lint_auto_find_test_dependencies()endif()ament_package() # 在这句之前添加可执行文件的构建配置此后的语句将不会被执行到为了能够生成可执行文件以及在编译时链接所需的依赖库需要在CMakeLists.txt中添加以下配置add_executable(my_node src/my_node.cpp) # 添加可执行文件指定源代码ament_target_dependencies(my_node rclcpp) # 指定可执行文件所依赖的库这里依赖rclcpp库install(TARGETS my_node DESTINATION lib/${PROJECT_NAME}) # 安装可执行文件到指定目录这部分需要一些CMake的基础知识可以通过我的博客从编译链接到Cmake以及find_package使用指南进行学习。值得注意的是下面的两种写法是相同的ament_cmake的写法add_executable(my_node src/my_node.cpp)ament_target_dependencies(my_node rclcpp)常见的CMake写法add_executable(my_node src/my_node.cpp)target_include_directories(my_node PUBLIC ${rclcpp_INCLUDE_DIRS})target_link_libraries(my_node ${rclcpp_LIBRARIES})在ROS2中其使用了ament_cmake作为构建系统的扩展提供了一些便捷的宏和函数来简化CMake的配置。ament_target_dependencies就是其中之一。但是虽然使用ament_target_dependencies可以简化CMake的配置但是这只适用于ament_cmake构建的package,即在package.xml中指定了build_typeament_cmake/build_type的package。如果是使用其他构建系统如catkin或纯CMake则需要使用传统的CMake方式来配置依赖关系.最后要使用install命令将可执行文件安装到指定目录这样在构建完成后可执行文件将被放置在install/lib/package_name目录下该目录是ROS2的标准安装路径方便在运行时找到可执行文件。install(TARGETS my_nodeDESTINATION lib/${PROJECT_NAME}) # 安装可执行文件到指定目录这里TARGETS指定要安装的目标文件DESTINATION指定安装路径${PROJECT_NAME}是CMake中的变量表示当前项目的名称这里将可执行文件安装到lib目录下。在ROS2中使用colcon build来对软件包进行编译和构建。colcon build命令会根据CMakeLists.txt中的配置调用ament_cmake工具来生成可执行文件并将其安装到指定的安装路径中。安装路径通常是install/package_name/lib/package_name其中package_name是软件包的名称。此后执行命令ros2 run package_name executable_name即可运行该节点。假设软件包名称为robot_package可执行文件名称为my_node则运行命令为ros2 run robot_package my_node。但是当你构建成功后执行该指令后会有会提示找不到包image这是因为ros2 run指令会在环境变量AMENT_PREFIX_PATH所在的路径中查找包但是构建完毕后我们并没有更新环境变量所以需要执行下面的命令来更新环境变量source install/setup.bash其中install/setup.bash是ROS2在构建完成后自动生成的一个脚本文件用于设置环境变量使得ROS2能够找到安装的包和可执行文件。执行该命令后AMENT_PREFIX_PATH环境变量将被更新会将安装路径添加到其中从而使得ros2 run robot_package my_node命令能够正确找到并运行节点。最后再来解决一个问题为什么install指令只指出安装路径是lib/P R O J E C T N A M E 而不是 i n s t a l l / {PROJECT_NAME}而不是install/PROJECTN​AME而不是install/{PROJECT_NAME}/lib/P R O J E C T N A M E , 但是安装后却可以将可执行文件安装到 i n s t a l l / {PROJECT_NAME},但是安装后却可以将可执行文件安装到install/PROJECTN​AME,但是安装后却可以将可执行文件安装到install/{PROJECT_NAME}/lib/P R O J E C T N A M E 目录下呢这是因为在 R O S 2 中 i n s t a l l 指令中的路径是相对 C m a k e 的安装前缀路径即 C M A K E I N S T A L L P R E F I X 这个变量的而当我们使用 c o l c o n b u i l d 调用 a m e n t c m a k e 进行构建时 C M A K E I N S T A L L P R E F I X 会被自动指向 i n s t a l l / {PROJECT_NAME}目录下呢 这是因为在ROS2中install指令中的路径是相对Cmake的安装前缀路径即CMAKE_INSTALL_PREFIX这个变量的而当我们使用colcon build调用ament_cmake进行构建时CMAKE_INSTALL_PREFIX会被自动指向install/PROJECTN​AME目录下呢这是因为在ROS2中install指令中的路径是相对Cmake的安装前缀路径即CMAKEI​NSTALLP​REFIX这个变量的而当我们使用colconbuild调用amentc​make进行构建时CMAKEI​NSTALLP​REFIX会被自动指向install/{PROJECT_NAME}/目录下的工作空间路径。可以通过查看build目录下的CMakeCache.txt文件来验证这一点。image最后总结一下使用ros2 pkg create命令创建软件包指定构建类型和依赖关系。在src目录下新建代码文件编写节点的源代码并在CMakeLists.txt中配置生成可执行文件并指定依赖关系。使用colcon build命令进行构建生成可执行文件并安装到指定路径。使用source install/setup.bash更新环境变量使得ROS2能够找到安装的包和可执行文件。注意使用colcon build命令最好在工作空间目录下执行而不是包内执行否则可能会导致构建失败或找不到包的问题且这样执行可以直接在工作空间下构建所有包而不需要进入每个包的目录进行构建同时也会考虑到包之间的依赖关系确保构建顺序正确。使用ros2 run package_name executable_name命令运行节点确保在运行前已经更新了环境变量。话题通信与接口上面我们说了节点之间可以通过话题进行通信。对于一个节点他可以作为话题的发布者Publisher或者订阅者Subscriber。发布者负责将消息发送到话题而订阅者则接收来自话题的消息。话题通信是ROS2中最常用的通信方式。话题通信所发布的消息类型是由接口(Interface)定义的接口定义了消息的结构和数据类型。ROS2提供了多种内置的消息类型如std_msgs、sensor_msgs等也支持自定义消息类型。可以将消息接口类比为一个定义好的数据包发布者和订阅者都需要遵循这个数据包的格式来进行通信。我们可以使用ros2 interface list命令来查看所有可用的接口类型使用ros2 interface show interface_name命令来查看某个接口的详细定义。比如查看std_msgs/msg/String接口的定义ros2 interface show std_msgs/msg/String输出结果如下string data这代表着std_msgs/msg/String这个接口定义了一个名为data的字符串类型字段发布者在发送消息时需要填充这个字段而订阅者在接收消息时也会获取到这个字段的值。其中string是ROS2中定义的基本数据类型之一表示一个字符串类型的数据。除了stringROS2还支持多种基本数据类型如int32、float64、bool等以及数组和自定义消息类型。其他基本类型见下表类型 说明 C对应类型bool 布尔 boolbyte 8位无符号整数 uint8_tchar 8位字符 uint8_tfloat32 32位浮点 floatfloat64 64位浮点 doubleint8 8位有符号整数 int8_tuint8 8位无符号整数 uint8_tint16 16位有符号整数 int16_tuint16 16位无符号整数 uint16_tint32 32位有符号整数 int32_tuint32 32位无符号整数 uint32_tint64 64位有符号整数 int64_tuint64 64位无符号整数 uint64_tstring 字符串 std::string