更好的阅读体验:https://www.foooor.com
3 HelloWorld
下面从 HelloWorld 开始,讲解 ROS2 的开发。
ROS 开发主要使用 C++ 或 Python 实现,如果要实现的功能,对性能有要求,可以使用 C++ 实现,如果对性能没有特别高的要求,可以使用 Python 实现,Python 的开发效率要高一些。
ROS1核心是C++03,而ROS2广泛使用C++11;ROS1 的Python使用Python2,ROS2 使用的Python版本至少是3.5及以上。
在开始之前,先介绍几个概念:工作空间、功能包、节点。
- 功能包
功能包类似于模块或子项目,它们可以被独立开发和复用,每个功能包通常有一个明确的功能目标,比如实现某种算法、驱动硬件等。
我们在开发机器人的时候,机器人的摄像头、雷达等硬件,厂家会提供 ROS 开发的功能包,我们只需要在我们的项目中集成厂家提供的 ROS 功能包,就能获取到硬件传感器的数据。同样,在开发机器人 SLAM 建图导航功能的时候,可以将建图封装在一个功能包中,导航封装在一个功能包中,这样也便于管理、复用和维护。
- 节点
节点是 ROS 中的执行单元,是一个正在运行的 ROS 程序。每个节点都独立运行,可以与其他节点通过 ROS 通信机制进行数据交换。
节点的职责是完成特定的功能,比如控制机器人、处理传感器数据、发布指令等,节点是在功能包中的,一个功能包中可以存在多个节点。
- 工作空间
工作空间是 ROS 项目开发的顶层目录,可以理解为一个项目,用来存放和管理多个功能包。它包含了所有与项目相关的代码、配置和依赖项。
三者关系:
- 工作空间是一个容器,它包含多个功能包。
- 功能包是功能代码的逻辑单元,里面可能包含多个节点。
- 节点是实际执行功能的程序,通过 ROS 的通信机制与其他节点协作完成系统功能。
所以在 ROS 中实现功能是通过节点来实现的,节点是在功能包中,一个功能包可以提供多个功能,功能包是在工作空间中进行管理的。
类似于 项目 -> 子模块 -> 功能
,所以在 ROS 中实现功能,从0开始创建,需要 创建工作空间 -> 创建功能包 -> 创建节点
。
3.1 创建工作空间
ROS2 中,工作空间的目录结构:
ros2_ws/ # 工作空间根目录,自定义名称
├── src/ # 源代码目录,存放所有的功能包
├── build/ # 编译目录,存放构建文件(colcon build 时生成)
├── install/ # 安装目录,存放可执行文件和已编译的依赖项(colcon build 时生成)
└── log/ # 日志文件目录,存放构建和运行时的日志信息
在进行 ROS 开发,首先需要创建工作空间,然后在工作空间的 src/
目录下创建功能包。开发完成通过 colcon build
命令将功能包编译到 install/
目录中,然后运行。
下面来创建工作空间。
可以使用界面,在你放置的位置创建工作空间的文件夹,或者使用终端命令来创建也是可以的,工作空间的名称是自定义的。
例如我在主目录(~表示主目录)创建一个 ros2_ws 的工作空间:
mkdir ~/ros2_ws # 创建文件夹
cd ros2_ws # 进入到 ros2_ws 目录中
mkdir src # 在 ros2_ws 目录下创建 src 目录
这样工作空间就创建好了,其实就是创建了两个目录。
创建完成,可以进入到工作空间下进行构建:
cd mkdir ~/ros2_ws
colcon build
构建完成,工作空间下就会存在 src、install、build、log 四个目录了。
3.2 创建功能包
功能包是在工作空间的 src 目录下创建的。ROS2 中功能包需要指定使用的语言,创建功能包使用如下命令:
# 首先进入到工作空间的 src 目录下
cd ~/dev_ws/src# 创建C++功能包
ros2 pkg create --build-type ament_cmake hello_pkg_cpp# 或者,创建Python功能包
ros2 pkg create --build-type ament_python hello_pkg_python
ros2 pkg create
:ROS2 中创建功能包的命令--build-type
:表示新创建的功能包是C++还是Python的,如果使用C++或者C,参数值为ament_cmake,如果使用Python,值为ament_python;- 命令后面的
hello_pkg_cpp
和hello_pkg_python
表示的是功能包的名称。 - 功能包的名字不要包含
-
,可以使用_
。
这样就会在工作空间下的 src
目录下创建功能包,每个功能包下都会有一个 package.xml 文件,里面有功能包的描述和功能包的依赖信息等。
3.3 Python实现节点
下面就来实现一个简单的节点,功能很简单,就是循环打印 Hello World!
,主要是看如何实现一个节点。
在上面已经创建了两个功能包,hello_pkg_cpp
和 hello_pkg_python
。
下面在 hello_pkg_python
功能包下,使用 Python 实现一个节点。
1 创建节点文件
在 hello_pkg_python/hello_pkg_python
目录下创建 python 文件,名称自定义。
例如我这里叫 hello_world.py
,内容如下:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-import rclpy # 导入 rclpy 库,这是 ROS2 的 Python 接口
from rclpy.node import Node # 导入 Node 类,这是 ROS2 的节点类
import time # 导入 time 模块,用于实现线程的休眠def main(args=None): # main 函数是 ROS2 节点的入口函数,参数 args 通常保留为 None,表示不需要额外的启动参数# 1.创建节点rclpy.init(args=args) # 初始化 ROS2 Python 客户端库,必须在创建节点之前调用node = Node("hello_node") # 创建一个名为 "hello_node" 的节点# 2.实现功能while rclpy.ok(): # 当 ROS2 系统处于正常运行状态时,循环会持续执行node.get_logger().info("Hello World!") # ROS2中打印日志time.sleep(1) # 线程休眠 1 秒,防止打印过于频繁# 3.销毁节点node.destroy_node() # 销毁节点,释放资源rclpy.shutdown() # 关闭 ROS2 客户端库
实现节点主要分为三个步骤:
- 创建节点
- 实现功能,循环打印
Hello World!
。 - 销毁节点
2 配置文件可执行权限
需要授权 python
文件可执行权限:
# 进入到python文件目录
cd src/hello_pkg_python/hello_pkg_python
chmod +x hello_world.py
3 配置节点
在功能包下的 setup.py 中配置节点,在 entry_points
中配置:
entry_points={'console_scripts': ['hello_node = hello_pkg_python.hello_world:main',],
},
上面就是配置节点运行的是哪个 Python 文件的哪个方法。前面的 hello_node
是自定义的,建议和 Node("hello_node")
对应。
一个功能包下是可以创建多个节点的,创建多个 Python 文件即可,进行同样的配置。
4 构建项目
现在代码已经编写完成了,需要构建项目。
在工作空间下执行如下命令:
colcon build
构建完成,会在 install
目录下生成文件。
也可以使用如下命令,单独编译一个功能包:
colcon build --packages-select hello_pkg_python
5 运行节点
首先执行 source
命令,在工作空间下执行:
source install/local_setup.sh
执行的作用主要是为了让 ros2 命令可以找到我们的功能包。
每次都执行上面的命令有些麻烦,我们可以将命令添加到环境变量中,这样每次打开终端就能 source
一下,就不用每次都运行 source
了。
source /opt/ros/humble/setup.bash # 安装ros2的时候已经添加了,指定ROS2的路径
source ~/ros2_ws/install/local_setup.sh # 指定工作空间的路径,让ROS可以找到我们的功能包
然后使用如下命令启动节点:
ros2 run hello_pkg_python hello_node
ros2 run
:这个是 ros2 中运行节点的命令;hello_pkg_python
:是功能包的名字;hello_node
:是节点的名称。
执行效果如下,每秒打印 Hello World!
:
在 ROS 1 中,roscore
是一个非常关键的组件。它主要用于提供名称服务(ROS Master)、参数服务器等功能。节点在启动时需要向roscore
中的 ROS Master 进行注册,以便让其他节点能够发现它。所以在 ROS1 中启动节点前,需要先运行 roscore
。
但是ROS2 采用了一种分布式的发现机制,不再依赖于像roscore
这样的中央节点管理器。它使用了 DDS(Data - Distribution Service)中间件。DDS 是一种以数据为中心的发布 - 订阅通信标准,能够自动发现网络中的节点和话题。所以在 ROS2 中启动节点不需要roscore
,
3.4 C++实现节点
使用 C++ 实现节点也是类似的。
1 创建节点文件
在 hello_pkg_cpp/src
目录下创建 C++ 文件,名称自定义。
例如我这里叫 hello_world.cpp
,内容如下:
#include <rclcpp/rclcpp.hpp> // 导入 ROS2 C++ 客户端库
#include <chrono> // 导入时间处理库
#include <thread> // 导入线程库int main(int argc, char **argv) {// 1.创建和初始化操作rclcpp::init(argc, argv); // 初始化 ROS2 客户端库auto node = rclcpp::Node::make_shared("hello_node"); // 创建一个名为 "hello_node" 的节点// 2.实现功能while (rclcpp::ok()) { // 当 ROS2 系统正常运行时RCLCPP_INFO(node->get_logger(), "Hello World!"); // 打印日志信息 "Hello World!"std::this_thread::sleep_for(std::chrono::seconds(1)); // 线程休眠 1 秒,防止打印过于频繁}// 3.关闭节点rclcpp::shutdown(); // 关闭 ROS2 客户端库return 0; // 返回 0,表示程序正常结束
}
2 配置CMakeLists.txt
在功能包下的 CMakeLists.txt 中,添加 C++ 编译配置,可以添加在 find_package(ament_cmake REQUIRED)
下面:
# 找到此包需要的其他包
find_package(rclcpp REQUIRED)# 添加可执行文件
add_executable(hello_node src/hello_world.cpp)# 目标链接库
ament_target_dependencies(hello_node rclcpp)# 安装可执行文件
install(TARGETShello_nodeDESTINATION lib/${PROJECT_NAME}
)
节点的名称是 hello_node
,和 C++ 代码中对应。
一个功能包下是可以创建多个节点的,创建多个 C++ 文件即可,如果创建多个节点,那么在 CMakeLists.txt
中配置如下:
# 找到此包需要的其他包
find_package(rclcpp REQUIRED)# 添加可执行文件,指向 hello_world.cpp
add_executable(hello_node src/hello_world.cpp)
ament_target_dependencies(hello_node rclcpp)# 添加可执行文件,指向 print_word.cpp
add_executable(print_node src/print_word.cpp)
ament_target_dependencies(print_node rclcpp)# 安装可执行文件
install(TARGETShello_nodeprint_nodeDESTINATION lib/${PROJECT_NAME}
)
上面配置了 hello_node
和 print_node
两个节点。
3 构建项目
现在代码已经编写完成了,需要构建项目。
在工作空间下执行如下命令:
colcon build
构建完成,会在 install
目录下生成文件。
也可以使用如下命令,单独编译一个功能包:
colcon build --packages-select hello_pkg_cpp
4 运行节点
首先执行 source
命令,在工作空间下执行:
source install/local_setup.sh
如果 ROS 还找不到你的功能包和节点,你就 source 一下,如果找得到就不用了。
然后使用如下命令启动节点:
ros2 run hello_pkg_cpp hello_node
ros2 run
:这个是 ros2 中运行节点的命令;hello_pkg_cpp
:是功能包的名字;hello_node
:是节点的名称。
执行效果也是每秒打印 Hello World!
。
3.5 节点代码优化
上面在使用 Python 实现节点的时候,使用的是面向过程的编码方式,在实际的开发中,现在更推荐使用面向对象的编码方式。
1 优化Python代码
我们可以创建一个类继承 Node 类,然后类中实现我们的功能。
修改上面的 Python 代码如下:
#!/usr/bin/env python3
# -*- coding: utf-8 -*-import rclpy # ROS2 Python接口库
from rclpy.node import Node # ROS2节点类
import time # 线程用来休眠# 创建节点类,实现节点,具体的功能在类中实现
class HelloWorldNode(Node):def __init__(self):super().__init__('hello_node')def print_hello_world(self):while rclpy.ok():self.get_logger().info("Hello World!") # ROS2打印日志time.sleep(1) # 休眠1秒def main(args=None):# 初始化和创建节点rclpy.init(args=args)node = HelloWorldNode()# 调用打印node.print_hello_world()# 销毁node.destroy_node()rclpy.shutdown()
上面的代码使用面向对象的方式,将节点实现的具体功能放到类中。
2 优化C++代码
同样,也是用面向对象的编码方式优化 C++ 代码:
#include <rclcpp/rclcpp.hpp> // 引入 ROS2 C++ 接口库
#include <chrono> // 引入 chrono 库,用于时间处理
#include <thread> // 引入 thread 库,用于休眠线程// 定义一个 HelloWorldNode 类,继承自 rclcpp::Node 类
class HelloWorldNode : public rclcpp::Node
{
public:// 构造函数,初始化节点名称为 "hello_node"HelloWorldNode() : Node("hello_node") {}// 定义一个函数,用于每秒打印一次 "Hello World!"void print_hello_world(){// 使用 while 循环,检查节点是否正常运行while (rclcpp::ok()) {// 打印 "Hello World!" 消息到终端,带有日志信息RCLCPP_INFO(this->get_logger(), "Hello World!");// 休眠 1 秒钟,类似于 Python 的 time.sleep(1)std::this_thread::sleep_for(std::chrono::seconds(1));}}
};// main 函数是程序的入口点
int main(int argc, char * argv[])
{// 初始化 ROS2,必须在创建节点之前调用rclcpp::init(argc, argv);// 创建一个共享指针,指向 HelloWorldNode 节点对象auto node = std::make_shared<HelloWorldNode>();// 调用节点的 print_hello_world 函数,开始打印 "Hello World!"node->print_hello_world(); // 当循环结束时,关闭 ROS2 系统,释放资源rclcpp::shutdown(); // 返回 0,表示程序成功退出return 0;
}