ROS2 支持 Python 语言,本文记录 Python 创建 ROS2 节点的流程以及运行方式。
安装好 ROS2 环境
安装好 Python 环境 (python 3.10)
我安装的 ros2 humbel 自带了 python 3.10, 使用其他版本的 python 会报错
安装 colcon
1 | pip install -U colcon-common-extensions |
---|
ros2-python-test
,后续将在这里创建一系列子文件夹以用作不同的ROS2案例的工作空间。1 | mkdir ros2_ws_demo001/ |
---|
1 | cd ros2_ws_demo001 |
---|
确定了工作空间,就像盖房子已经选好了地址,打好了地基。
然后在这个地基上,就要开始搭建房子了,并且对房子进行各种定义。
ROS2是怎么搭建这个房子的呢?它是要先创建一个统一的固定名字的文件夹“src”,然后在这个文件夹下面再去具体定义各个房间。
12 | mkdir srccd src |
---|
在src文件夹下面定义各个房间早已经工程化了,直接在命令行属于ros2相关命令就能快速搭建好一个基础框架出来。
1 | ros2 pkg create package_001 --build-type ament_python --dependencies rclpy |
---|
ros2 会创建一系列 py 框架文件,当前文件结构如下:
12345678910111213141516 | .└── src └── package_001 ├── package_001 │ └── __init__.py ├── package.xml ├── resource │ └── package_001 ├── setup.cfg ├── setup.py └── test ├── test_copyright.py ├── test_flake8.py └── test_pep257.py5 directories, 8 files |
---|
虽然一下多了很多文件夹和文件,但是这些都是一个功能包的标配。
我们只需要在 package_001
中定义好一个节点,然后再在 setup.py
文件中配置好我们要调用这个节点就可以了。
package_001
中创建 node_001.py
文件,写入如下内容:123456789 | import rclpyfrom rclpy.node import Nodedef main(args=None): rclpy.init(args=args) # init rclpy my_node = Node("node_001") # to create a Node object my_node.get_logger().info("hello, I am node_001") # to print a message. rclpy.spin(my_node) # to keep Node running rclpy.shutdown() # to close Node |
---|
setup.py
。console_scripts的值原本为空,这里需要将console_scripts的值添加上我们新建的节点,如下所示:
123 | 'console_scripts': "node_001 = package_001.node_001:main" , |
---|
保存之后退出 setup.py
文件。
到这里终于把源码部分定义好了(相当与定义好了房间的框架),接下来,只需要编译一下这个工作空间(相当于快速填充墙体),一个最简单的房子就搭建好了。
ros2_ws_demo001
下,执行如下命令进行编译就行了。1 | colcon build |
---|
终端输出:
12345 |
|
---|
编译完成后,我们发现工作空间下又多出了几个文件夹。分别是build、install、和log。而在这几个文件夹下面又很多其他文件夹和文件生成。
12 | source install/setup.zshros2 run package_001 node_001 |
---|
可以看到屏幕上打印了“hello,I am node_001”,这个正是我们在自定义节点里做的事。
12 |
|
---|
12 |
|
---|
我们来稍微总结下使用python创建一个ros2节点并运行的整个过程:
1、创建一个独立的工作空间(其实就是创建了一个文件夹);
2、在工作空间下创建src文件夹(源码文件夹),并在src下创建功能包(非常简单,使用ros2命令行工具直接就生成了);
3、在功能包里面编辑一个节点python程序,唯一需要你动动脑子写的东西,但我们这个例子也才区区8行代码,已经简单到了极致;
4、在功能包里面配置setup.py文件,将我们上一步创建的节点程序调用起来;
5、回到工作空间,使用colcon工具编译(build)整个代码;
6、运行编译后的代码;
事实上 Python 使用 ROS2 总线相对灵活,不一定需要上述 ros2 run
的方法,直接 Python 运行某个 py 文件也是一样的。
1234567891011121314 | import rclpyfrom rclpy.node import Nodedef main(args=None): rclpy.init(args=args) # init rclpy my_node = Node("node_001") # to create a Node object my_node.get_logger().info("hello, I am node_001") # to print a message. rclpy.spin(my_node) # to keep Node running rclpy.shutdown() # to close Nodeif __name__ == "__main__": main() |
---|
你可以自己试一下。
稍微扩展下上面的代码,做两个ROS2节点,然后运行起来。
12 | cp -rf ros2_ws_demo001/ ros2_ws_demo002_2_nodes/cd ros2_ws_demo002_2_nodes |
---|
12 | cp src/package_001/package_001/node_001.py src/package_001/package_001/node_002.pygedit src/package_001/package_001/node_002.py |
---|
修改 node_002.py
123456789 | import rclpyfrom rclpy.node import Nodedef main(args=None): rclpy.init(args=args) # init rclpy my_node = Node("node_002") # to create a Node object my_node.get_logger().info("hello,I am node_002") # to print a message. rclpy.spin(my_node) # to keep Node running rclpy.shutdown() # to close Node |
---|
把它配置到setup.py文件中:
1234 | 'console_scripts': "node_001 = package_001.node_001:main", "node_002 = package_001.node_002:main" , |
---|
然后,退回到新的工作空间下,删除之前的编译结果,重新进行编译。
12345 | cd ~/ROS2_study/ros2_ws_demo002_2_nodesrm -rf buildrm -rf installrm -rf logcolcon build |
---|
编译完成后,在当前终端直接启动第一个节点:
12 | source install/setup.zshros2 run package_001 node_001 |
---|
然后新打开一个终端,运行第二个节点:
12 | source install/setup.zshros2 run package_001 node_002 |
---|
两个终端页面显示了我们输出的字符串:
1234567 | terminal 1> ros2 run package_001 node_001INFO node_001: hello, I am node_001# terminal 2> ros2 run package_001 node_002INFO node_002: hello, I am node_002 |
---|
然后再来打开一个终端确认下目前正在运行的节点有哪些:
1 | ros2 node list |
---|
可以看到两个节点在线
123 |
|
---|
至此,我们实现了两个ROS2节点的创建并完成了调用。
launch 文件默认 调用的 py 文件中存在 main() 函数,他会自动调用 main 函数,并将其中生成的节点命名为 launch 文件中配置的名称
123456 | Node( package='package_001', executable='node_001', name='node_001_launch', output='screen',), |
---|
该配置会在 package_001
文件夹中,调用 node_001.py
文件, 调用其中的 main()
函数,将生成的节点命名为 node_001_launch
**,调用的 Python 为当前终端环境中的默认 python 程序路径,也就是说, 通过** conda activate
的环境可以作用于 ros2
ROS2 可以通过配置 launch
文件,一次性将所有节点都设置好,然后统一启动。
12 | cp -rf ros2_ws_demo002_2_nodes/ ros2_ws_demo003_2_nodes_launch/cd ros2_ws_demo003_2_nodes_launch/ |
---|
1 | mkdir src/package_001/launch |
---|
my_multi_nodes_launch.py
, 内容如下:1234567891011121314151617181920 | my_multi_nodes_launch.pyfrom launch import LaunchDescriptionfrom launch_ros.actions import Nodedef generate_launch_description(): return LaunchDescription( Node( package='package_001', executable='node_001', name='node_001', output='screen', ), Node( package='package_001', executable='node_002', name='node_002', output='screen', ), ) |
---|
12345678910111213141516171819202122232425262728 | from setuptools import setuppackage_name = 'package_001'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/my_multi_nodes_launch.py'), ], install_requires='setuptools', zip_safe=True, maintainer='goodman', maintainer_email='goodman@todo.todo', description='TODO: Package description', license='TODO: License declaration', tests_require='pytest', entry_points={ 'console_scripts': "node_001 = package_001.node_001:main", "node_002 = package_001.node_002:main" , },) |
---|
实这里只添加了一句话:
1 | ('share/' + package_name, 'launch/my_multi_nodes_launch.py'), |
---|
最后,回到工作空间编译一下:
12345 | cd ros2_ws_demo003_2_nodes_launch/rm -rf buildrm -rf installrm -rf logcolcon build |
---|
12 | source install/setup.zshros2 launch package_001 my_multi_nodes_launch.py |
---|
终端输出:
1234567 |
|
---|
123 |
|
---|
至此我们通过launch方法实现了一次性启动多个节点。
可以在 一个 py 文件中直接启动多个 ros2 节点
123456789101112131415161718 | import rclpyfrom rclpy.node import Nodeclass MyNode(Node): def __init__(self, name): super().__init__(name) self.get_logger().info(f'Hello from {name}')def main(args=None): rclpy.init(args=args) node1 = MyNode('Test_node1') node2 = MyNode('Test_node2') rclpy.spin(node1) rclpy.spin(node2) rclpy.shutdown()if __name__ == '__main__': main() |
---|
这样在运行这个 py 程序后可以看到 Test_node1
Test_node2
两个节点在线.
但是如果将该文件替换上文的 node_001.py 文件,则会报错:
1 | node_001-1 1720692076.228787954: Publisher already registered for provided node name. If this is due to multiple nodes with the same name then all logs for that logger name will go out over the existing publisher. As soon as any node with that name is destructed it will unregister the publisher, preventing any further logs for that name from being published on the rosout topic. |
---|
这时查看 ros2 的节点列表看到的是:
12345 |
|
---|
也就是说通过 ros2 launch 启动方式启动和 运行 py 文件启动的ros 节点结果可能是不同的, 关键在于launch 配置, 将过程中创建的节点都命名为配置的名称.