我们已经知道,在ROS2中,节点是相当重要的,所以这里我们将讲解一个简单的节点是如何构建的
节点的构建
首先简单介绍一下RCLCPP和RCLPY:
这两个作为头文件或者库文件在我们的功能包中往往是相当常见的,分别应用于C++和Python编写的节点,作为客户端库。在其中,会提供相应的通讯接口和相应模块,方便我们对系统的构建
在我们使用的这个库中就已经有对于结点(Node)的定义,所以我们可以直接调用这里面的函数来创建我们具有多通讯功能的节点
这里我们可以看到我们的节点具有很多已经封装好的方法,一口气学完是不可能的,所以我们在后续通讯方式的学习中取用需要的内容进行学习即可
下面先后给出cpp和py的实例,再对代码的细节做具体的解释:
(这里先不要跟着抄代码和运行,我们先看基本的结构)
C++:
#include "rclcpp/rclcpp.hpp"
int main(int argc, char ** argv)
{
rclcpp::init(argc,argv);
auto node = std::make_shared<rclcpp::Node>("node_01");
RCLCPP_INFO(node->get_logger(),"hello world!");
rclcpp::spin(name);
rclcpp::shutdown();
return 0;
}
- 调用我们的头文件对于 rclcpp 文件的引用进行部署
- main()中传入参数 argc 和 argv ,其中 argv 相对重要,作为命令行中传入的参数值,从1开始(因为 0 被本身占位),每一个参数以 char 类型呈现,可以通过函数进行转换
- rclcpp::init() 主要对于后续使用的节点内通讯工具进行初始化部署
- auto为自动类型推导,make_shared 为共享指针,括号中传入的是节点名称
- RCLCPP_INFO 是节点发送信息的一种类型,node->get_logger()调用了节点发送信息的日志,后面传入的就是发送信息的内容
- rclcpp::spin(name) 保持节点运行,检测是否有输入退出指令(Ctrl+C)
- rclcpp::shutdown() 关闭
Python:
import rclpy
def main(args=None):
rclpy.init(args)
node = rclpy.create_node("node_02")
node.get_logger().info("hello world!")
rclpy.spin(node)
rclpy.shutdown()
- import rclpy 导入 rclpy 的一些方法
- def main() 设置函数 main()(后面会作为接口函数来使用,这里先不做过于详细的解释,后面配置文件再具体说明)
- rclpy.init() 对于节点的通讯接口等进行初始化
- create_node 同样是创建节点,同时设置节点名
- get_logger().info(...) 同样是调用了日志,进行信息发送
- rclpy.spin(node) 保持节点运行,检测是否有退出指令(Ctrl+C)
- rclpy.shutdown() 关闭节点
可以看到的是,在上面的节点实例中,节点的构造逻辑和结构基本是一致的
节点的基础文件配置:
我们的节点编译需要相应的文件结构,根据前文给出的介绍,如果要对节点进行构建,需要先构建工作空间,再有功能包,最后才是节点
下面我们分 cpp 和 py 讲解文件的结构:
CPP文件结构
先创建一个文件夹:
mkdir -p ros2_learning/node_learning/
cd ros2_learning/node_learning
mkdir -p node_demo_ws/src/
在上面的文件结构中,/ros2_learning 是我们学习用的一个大文件包,这里面分出了不同的学习内容,今天学习node的创建,所以我们使用 /node_learning,在此之下创建工作空间 /node_demo_ws,然后创建源文件的文件夹 /src
接下来我们使用ros2内置的包创建方法来创建cpp功能包结构
cd node_demo_ws/src
ros2 pkg create node_example_cpp --build-type ament_cmake --dependencies rclcpp
这里跳转到 /src 后,我们调用 pkg create,创建了名为 node_example_cpp 的功能包,类型为 ament_cmake ,并添加了 rclcpp 的依赖
在我们的功能包创建完成后,我们以/src为根的文件目录如下:
.
└── src
└── node_example_cpp
├── CMakeLists.txt
├── include
│ └── node_example_cpp
├── package.xml
└── src
5 directories, 2 files
接下来,我们在 src 目录下创建 node_01.cpp 文件,文件结构就变成了这样:
.
└── src
└── node_example_cpp
├── CMakeLists.txt
├── include
│ └── node_example_cpp
├── package.xml
└── src
└── node_01.cpp
5 directories, 3 files
这个时候就可以把我们的节点代码写入 node_01.cpp 中了
这个时候还不能直接运行,需要先修改 cmake 文件和 package 文件来添加文件路径和相应依赖:
CMakeList.txt
这个文件的名称是不能改变的,可以简单阅读里面的内容,可以发现有许多类似于提供依赖的语句,这个后面再来细讲
我们先走到CMakrList的最后一句前面,加入如下两段代码:
add_executable(node_01 src/node_01.cpp)
ament_target_dependencies(node_01 rclcpp)
上面两段代码的含义:
- add_executable([node_name] [file_list]) 通过文件结合生成可执行的节点文件
- amrnt_target_dependencies([node_name] [dependencies_list]) 向节点添加需要的依赖列表
install(TARGETS
node_01
DESTINATION lib/${PROJECT_NAME}
)
上面的代码含义:
表示安装 node_01 这个可执行文件(目标),其中 &{PROJECT_NAME} 作为 CMake 的内置变量,表示当前项目的名称
这里 package.xml 不需要做其他的修改,保持功能包生成时的原样即可
如何编译运行
我们需要依次运行下面三段命令行(注意运行的时候命令行所处的位置应该是 /node_demo_ws):
colcon build
source install/setup.bash
ros2 run node_example node_01
- colcon build 表示对于当前工作空间下的所有文件进行编译
- source install/setup.bash 用于加载当前工作空间的环境变量,可以保证 ros2 在编译后能对你的其他命令进行识别
- ros2 run [package] [node] 表示对于某个功能包中的某个可执行节点进行运行
注意:
- source install/setup.bash 在每次编译之后最好都要重新操作一遍,防止出现奇怪的问题
接下来你就可以在命令行输入ros2 node list来查看现在正在运行的所有节点
现在再看你的文件结构,会发现工作空间下多了三个冒昧的家伙:/log、/build 和 /install
如果某次你的代码出现了玄学错误,那么可以尝试删除这三个冒昧的家伙,有可能代码的功能就正常了(
Py文件结构:
我们的文件结构在上方时相近的,所以我们可以依赖上面CPP中创建的文件结构继续构建 py 的结构:
cd /node_demo_ws/src/
ros2 pkg create node_example_py --build-type ament_python --dependencies rclpy
这里的第二个语句相信你可以自己解释了
接下来我们能看到如下的文件结构:
.
├── node_example_py
│ └── __init__.py
├── package.xml
├── resource
│ └── node_example_py
├── setup.cfg
├── setup.py
└── test
├── test_copyright.py
├── test_flake8.py
└── test_pep257.py
3 directories, 8 files
接下去我们在 node_example_py 之下创建 node_02.py,文件结构如下:
.
├── node_example_py
│ ├── __init__.py
│ └── node_02.py
├── package.xml
├── resource
│ └── node_example_py
├── setup.cfg
├── setup.py
└── test
├── test_copyright.py
├── test_flake8.py
└── test_pep257.py
3 directories, 9 files
在 node_02.py 中,我们就可以加入我们的节点代码了
接下来配置 setup.py 文件(修改如下部分):
entry_points={
'console_scripts': [
"node_02 = node_example_py.node_02:main"
],
},
)
这里声明了一个功能包下的节点(等号右边的结构),名为 node_02(等号左边那个),同时提供了入口函数为 main 函数
这相当于为 colcon build 提供了一个固定的路径,编译的时候文件方向就能明确了
接下来我们编译运行我们的 python 节点(注意同样需要在WorkSpace中运行,不要走错路径):
colcon build
source install/setup.bash
ros2 run node_example_py node_02
这上面的三句话已经很明确了,这里就不多解释了
你同样可以使用ros2 node list来对运行中的节点进行查看
继承的方式构筑节点
我们在真实的应用场景中,往往不会使用上面的方法对节点进行创建,因为RCL提供的原装节点Node类具有的功能基本只有基础的通讯,无法很好的满足我们的实际应用需求,在前面我们也介绍了在我们的模块化编程中,OOP(面向对象编程)是一种相当重要的思想,所以我们这里需要使用继承的方法在Node的基础上创建自己的新节点
接下来同样先展示代码,再解释代码内容:
C++:
#include "rclcpp/rclcpp.hpp"
class MyNode01 : public rclcpp::Node {
public:
MyNode01(std::string name) : Node(name) {
RCLCPP_INFO(this->get_logger,"Hello,I'm %s",name.c_str());
}
private:
};
int main(int argc,char **agrv) {
rclcpp::init(argc,argv);
auto node=std::make_shared<MyNode01>("mynode_01");
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
- 上面共享指针的类型变成了 MyNode01
- MyNode01 继承自公共的 Node 类型,并调用构造函数向 Node 提供 name 参数
- 这里在构造函数中的 this 用于指向自身,不写也是不会报错的,但是写上结构会更清晰,方便后续迭代
- 这里一个小细节,我们的正则表达式中 %s 表示的是c类型的字符串,所以我们这里需要用 .c_str() 来进行风格转换(当然,不写也只是报一个 WARNING)
其余文件配置和编译的过程与之前都是一样的,只是如果你不想把一个大文件夹中的所有文件同时编译的话,可以在编译的时候添加参数如下:
colcon build --packages-select [package_list]
这样可以仅针对某些功能包进行编译
Python:
import rclpy
from rclpy.node import Node
class MyNode02(Node):
def __init__(name):
super.__init__(name)
self.get_logger().info("大家好,我是%s!" % name)
def main(args=None):
rclpy.init(args=args)
node = MyNode02("mynode_02")
rclpy.spin(node)
rclpy.shutdown()
- class 对 MyNode02 继承自Node类型进行定义
- MyNode02 定义构造函数,super.init() 调用父类构造函数传入 name 参数
- self. 表示自己,和 C++ 中 this-> 的含义是一样的
其余的配置方法与先前是一致的
那么相信你现在已经有自己创建自己的节点的能力了,接下来我们将进入ROS2四种主要通讯方式的学习
浙公网安备 33010602011771号