ROS2基础:节点、功能包、工作空间与其编译运行
src:ROS2新书测评活动,Launch官仓-Github
QS:需要下载编译器:sudo apt install python3-colcon-common-extensions
[TOCM]
介绍:本篇博客将会介绍ROS2创建/编译/运行一个节点必备的知识,包括使用Launch的节点批量启动
注意:命令行代码默认项目已经source install/setup.bash
,执行的工作目录为工作空间my_project
从创建一个节点开始
ROS2将节点-Node
视为机器人的细胞,每个节点都是一个独立的可执行文件/功能部件
。比如对于一个摄像头节点,他的职能就是拍摄图片发布到数据空间-DataBus
,之后,需要这些数据的节点,比如显示屏节点,或者实体分割节点会订阅这些数据进行显示或处理。在ROS2中,每个节点独占一个进程,且由于ROS2的节点管理是基于哈希表名称映射的,所以每个节点必须有一个唯一的名称。
工作空间
工作空间-WorkSpace
,就类比一个项目目录-ProjectDir
,或者一个工作台,或者一个工具箱。如果拿其他技术栈来比喻:
ROS2 |
前端页面 |
后端服务 |
游戏开发 |
建模渲染 |
智能项目 |
工作空间 |
Project |
Project |
Project |
Project |
工作空间 |
功能包 |
View |
App/Service |
场景 |
渲染链 |
Notebook |
节点 |
Component |
Router/API |
单个脚本材质贴图等 |
合成器节点 |
单模型 |
即:节点可以独立存在,且可以独立运行,但是得借由多个节点组成的包才是一个完整的系统,一个包可以有一个或多个节点,一个工作空间涵盖了多个包以及其之下的节点。
创建一个工作空间就是新建一个存放此新工程文件的文件夹:
如果是新工程,则可以colcon build --packages-select=false
初始化此my_project
工作空间,初始化会创建空的install
, build
, log
目录,并设置初始的install/setup.bash
,加载此bash环境后,ros2命令将会额外检测此工作空间下定义的消息、节点等等。
source install/setup.bash
如果是旧工程,可以使用ROS2自带的rosdep命令自动安装src代码空间中各功能包的依赖库:
rosdep install --from-paths src --ignore-src -r -y
功能包
一个功能包-Package
,一般是一个可独立运行的完整组件,内含有一个或者多个节点,比如移动控制、视觉感知、自主导航等等,将完整机器人分割为功能包使用了解耦的思想,功能包可以独立方向至其他机器人项目。
ros2 pkg create --build-type <build-type> <package-name>
build-type: ament_cmake | ament_python,根据适合/喜欢的语言进行选择
package-name: 功能包名,即src文件夹下将会新建什么名字的文件夹(功能包)
ros2 pkg create --build-type ament_python my_package
一个功能包含有一个描述元信息的package.xml
,包括包的版权、作者信息、开发日期、功能描述等等。另外还有一个本地化包含有的包构建配置文件,比如Cmake包含有的CMakeLists.txt
,Python包含有的setup.py
。其中使用方式与各语言通用包构建发布的配置一致。
节点
节点是一个单独的可执行文件,其中无论是C++还是Py,此文件将会启动一个Ros::Node
对象,比如:
from rcipy.node import Node
if __name__ == '__main__':
node = Node("node_name")
rclpy.spin(node)
node.destory_node()
rclpy.shutdown()
#include <rclcpp/rclcpp.hpp>
int main(int argc, char* argv[]) {
rclcpp::init(argc, argv);
auto node = rclcpp::Node("node_name");
rclcpp::spin(std::shared_ptr(node));
rclcpp::shutdown();
return 0;
}
但是,启动一个空节点没有太大的实际意义,所以一般启动都是实现特定功能的节点子类:
class MyNode(rclpy.Node):
def __init__(self, name: str, ...):
super.__init__(name)
...
...
class MyNode: public rclcpp::Node
{
public:
MyNode(const string &name, ...): Node(name)
{
...
}
...
}
编译运行
启动节点不是单纯的运行写好的节点文件,而是需要合理配置编译选项后,编译,通过ros命令来启动:
ros2 run <工作空间名字> <节点启动名>
其中需要设置的编译选项:
...
entry_points = {
'console_scripts': [
"node_class_name = my_package.my_node:main"
],
}
...
...
find_package(rclcpp REQUIRED)
add_executable(emm src/my_node.cpp)
ament_target_dependencies(emm rclcpp)
install(
TARGETS
emm
DESTINATION lib/${PROJECT_NAME}
)
...
编译操作:
colcon build
colcon build --packages-select <package_name> # 只编译指定功能包
colcon build --packages-up-to <package_name>
清除编译:
rm -rf install/ build/ log/
[==========]
到批量启动多个节点
前情提要:节点是一个独立进程
=意味着=> 每启动一个节点都会占用一个shell
,除非nohup后台启动或者使用容器等等虚拟化手段启动。当你需要启动的节点大于5个时,你会发现tmux也会变得不好使,那么如何使用一个命令行启动一键多个节点?且可以不用每次启动都麻烦的配置每个节点的启动配置?哈哈,ROS人有自己的Makefile:Launch
!
Launch
Launch,多节点启动与配置脚本,官方仓库:Github
,官方说明:design.ros2.org
。Launch启动文件使用Python进行描述,可以配置命令行输入的各项参数,并同时使用Python原有的编程功能。注意:Launch的功能是利用模板生成命令行命令,类似于模板引擎展开为界面代码一样,并不影响节点源码的行为!但是可以通过条件判断来在生成命令的同时更改命令行参数(类似于条件编译),或者使用循环批量生成大量的节点进行启动。
基础使用示例
import os
from launch import LaunchDescription
from launch_ros.actions import Node
from ament_index_python.packages import get_package_share_directory
def generate_launch_description():
return LaunchDescription([
Node(
package='my_py_package1',
executable='my_node1',
name='my_node1',
output='screen'
),
Node(
package='my_py_package1',
executable='my_node2',
name='my_node2',
output='screen'
),
*[Node(
package='my_cpp_package',
executable='my_node3',
name=f'cpp_node{i}',
output='screen'
) for i in range(3)],
])
Launch的编译和启动:
Launch文件在使用前需要移动至install
,但是直接在此文件夹下创建launch.py文件,在清理编译文件时会一不小心全删掉,所以一般放在功能包-Package目录下的launch文件夹下,但是如果图省事直接放于功能包-Package根目录,则常习惯命名为xxx.launch.py
来表示这是一个launch描述文件,之后配置相应的编译指导文件,将其作为资源文件直接拷贝到install/share
即可。
...
data_files = [
(
os.path.join('share', package_name, 'launch'),
glob(os.path.join('launch', '*.launch.py'))
), ...
]
...
...
install(DIRECTORY
launch
DESTINATION share/${PROJECT_NAME}/
)
...
ros2 launch <工作空间名称> <launch文件名>
常用描述方法
综上所述,一个Launch启动文件可以用以下模板描述:
def generate_launch_description():
return LaunchDescription([ ... ])
其中常用的描述方法有:
from launch import LaunchDescription
from launch.actions import (
GroupAction,
IncludeLaunchDescription,
)
from launch_ros.actions import (
Node,
PushRosNamespace,
)
from launch.launch_description_sources import PythonLaunchDescriptionSource
from ament_index_python.packages import get_package_share_directory
def generate_launch_description():
node1 = Node(
package='my_py_package1',
executable='my_node1',
)
others = IncludeLaunchDescription(
PythonLaunchDescriptionSource([
os.path.join(
get_package_share_directory("环境中的其他包的包名"),
"launch", "xxx.launch.py",
)
])
)
other2 = IncludeLaunchDescription(
PythonLaunchDescriptionSource([
os.path.join(
get_package_share_directory("环境中的其他包的包名"),
"launch", "xxx.launch.py",
)
])
)
other2_with_ns = GroupAction(
actions=[
PushRosNamespace('namespace_233'),
other2,
]
)
return LaunchDescription([node1, others, other2_with_ns])
常用参数配置
Node(
package="节点所在的功能包",
executable="编译后的可执行文件名",
namespace="节点所在命名空间",
name="对节点进行重命名",
parameters=[...],
remappings=[("原资源路径", "映射资源路径"), ("/emm", "/asd/qwe"), ...],
arguments=['-d', '命令行参数'],
)
使用参数-Parameters:
from launch.actions import DeclareLaunchArgument
from launch.substitutions import LaunchConfiguration, TextSubstitution
def generate_launch_description():
background_r_arg = DeclareLaunchArgument(
'background_r', default_value=TextSubstitution(text='0')
)
"""假设剩余的background_g和background_b参数在config/emm.yaml文件:
/turtlesim2/sim:
ros__parameters:
background_g: 0
background_b: 0
"""
config_gb_yaml_path = os.path.join(
get_package_share_directory("my_project"),
'config', 'emm.yaml'
)
return LaunchDescription([
background_r_arg,
Node(
...,
parameters=[
config_gb_yaml_path,
{
'background_r': LaunchConfiguration('background_r')
},
]
),
])