ROS2 介绍
ROS 全称机器人操作系统,英文全称(Robot Operating System),但ROS本身并不是一个操作系统,而是可以安装在现在已有的操作系统上(Linux、Windows、Mac)上的软件库和工具集。
ROS出生于2007年,ROS的出现解决了机器人各个组件之间的通信问题,同时基于ROS的完善的通信机制,越来越多的优秀的机器人算法集成到了ROS中来。
设计一个轮式移动机器人,我们对其进行拆解。可以分为感知、决策、控制三个部分。
- 感知部分有:激光雷达、深度相机、IMU、里程计、碰撞感知、建图
- 决策部分有:路径规划(navigation)算法、定位算法
- 控制部分有:轮子驱动
机器人复杂之处就在于此,如果想要整个机器人可以跑起来,那么必须要有一个东西将上面的几个部分合理的连接到一起,这个东西就是ROS。
ROS 提供了一套通信机制,将机器人的各个组件连接成一个整体。其作用就像我们的身体的神经系统一样,通过神经系统将我们身体的各个部分接入大脑。
ROS 这套通信机制有一个Ros Master,所有节点(可以理解为某一个组件,比如:激光雷达)的通信建立必须经过这个主节点。
这种组合结构图如下:
一旦Ros Master主节点挂掉后,就会造成整个系统通信的异常,此时避障策略将会失效,如果机器人正在运行,碰到障碍物会径直撞上去,机毁人亡!
现在的ROS功能已经变得非常的丰富和强大。但随着对ROS功能上要求越来越多,一些原始的架构和设计不能够满足目前的使用需求,这也是ROS2出现的原因。
ROS2继承了ROS原有的优秀之处,同时又带来了很多新的功能,ROS2相对于ROS更加的强大。
那么中间层ROS2到底相对于ROS做了哪些优化呢?
-
去中心化master,ROS和ROS2中间件不同之处在于,ROS2取消了master节点。各个节点之间可以通过DDS的节点相互发现,各个节点都是平等的,且可以1对1、1对n、n对n进行互相通信。
-
采用DDS通信,使得ROS2的实时性、可靠性和连续性上都有了增强。
下图则是 ROS2 的一个 node graph ,其中包含三个 Node ,Node 之间通过 Topic 和 Service 建立连接进行通信。
ROS2 Node
Node 在 ROS2 中代表了组件,通常是ROS图中的计算单元;每个 Node 应该执行一个逻辑操作。Node 之间可以互相通信。Node 可以与同一进程内的其他节点、不同进程内的 Node 或不同计算机上的 Node 进行通信。
ROS2 提供了 Topic, Service, Action 三类通信机制,Node 通常是 publisher、 subscriber、 Service server、 Service client、Action server 和 Action client 的复杂组合,同时具备这些功能。
- Node 可以 publish Topic 到具有名称的 Topic 以向其他 Node 传递数据;
- Node 可以 subscribe Topic 具有名称的 Toipc 从其他 Node 获取数据;
- Node 可以充当 Service client,以使另一个 Node 代表它们执行计算;
- Node 可以充当 Serivce server,为其他 Node 提供功能。对于长时间运行的计算;
- Node 可以充当 Action client,以使另一个 Node 代表它们执行计算;
- Node 可以充当 Action server,为其他 Node 提供功能;
- Node 可以提供可配置的 Parameters 以在运行时更改行为。
Node 之间的连接是通过分布式 发现 过程建立的。
Node 的发现通过 ROS2 的底层中间件自动进行。其过程可以总结如下:
- 当启动一个 Node 时,它会向在同一个 ROS 域(使用 ROS_DOMAIN_ID 环境变量设置)上的其他 Node 广播自己的存在。Node 会对此广播进行响应,并提供有关自身的信息,以便建立适当的连接并进行通信。
- Node 定期广播其存在,以便在初始发现期之后能够与新发现的实体建立连接。
- Node 在离线时向其他节点进行广播。
只有当 Node 具有兼容的 Quality of Service 设置时,它们才会与其他节点建立连接。
以 talker-listener demo 为例。在一个终端中运行 C++ talker 节点会发布消息到一个主题上,而在另一个终端中运行的 Python listener 节点会订阅相同主题上的消息。
你应该看到这些节点会自动发现彼此,并开始交换消息。
ROS2 Interface
在上面的 Node 介绍中, Node 之间存在的通信机制有 Topic, Service, Action 三类。 ROS2 使用一种简化的描述语言,即接口定义语言 (IDL) 来描述这三类通信的具体接口定义。 此描述使 ROS 工具可以轻松地自动生成多种目标语言的接口类型源代码。
msg
消息在 ROS 包的“msg/”目录中的“.msg”文件中描述和定义。 “.msg”文件由两部分组成:字段和常量。它们用于生成不同语言的消息源代码。
- 字段: 每个字段由一个类型和一个名称组成,以空格分隔,即:
fieldtype1 fieldname1
fieldtype2 fieldname2
fieldtype3 fieldname3
- 常量: 每个常量定义都类似于具有默认值的字段描述,但该值永远无法通过编程更改。 此值分配通过使用等号“=”来表示,例如
constanttype CONSTANTNAME=constantvalue
srv
Service 在 ROS 包的 srv/ 目录中的 .srv 文件中描述和定义。
Service 描述文件由请求和响应消息类型组成,以 --- 分隔。
任何两个以 --- 连接的 .msg 文件都是合法的 Service 描述。
<request_type> <request_fieldname>
---
<response_type> <response_fieldname>
这是一个非常简单的 Service 示例,它接受一个字符串并返回一个字符串:
string str
---
string str
action
Action 是一种长时间运行的请求/响应通信,其中 Action client(请求者)正在等待 Action server(响应者)采取某些 Action 并返回结果。 与 Service 相比,Action 可以长时间运行(数秒或数分钟),在发生时提供反馈,并且可以中断。
“.action”文件描述操作。它们由三部分组成:请求、响应和反馈。定义具有以下形式:
<request_type> <request_fieldname>
---
<response_type> <response_fieldname>
---
<feedback_type> <feedback_fieldname>
与服务一样,请求字段位于第一个三连字符(“—”)之前,而响应字段位于其之后。在第二个三连字符之后还有第三组字段,这是发送反馈时要发送的字段。
请求字段数量可以是任意数量(包括零个)、响应字段数量可以是任意数量(包括零个),反馈字段数量也可以是任意数量(包括零个)。
一个用于计算斐波那契数列的动作,具有以下接口:
int32 order
---
int32[] sequence
---
int32[] sequence
动作服务器接收此消息,开始计算直到 order 的数列(并提供反馈),最后在 sequence 中返回完整结果。
动作客户端是一个实体,将请求远程动作服务器代表其执行某些过程。动作客户端是创建包含 order 的初始消息,并等待动作服务器计算序列并返回(在此过程中提供反馈的)实体。
与动作服务器不同,可以使用相同的动作名称拥有任意数量的动作客户端。
ROS2 Parameters
ROS 2 中的参数与各个节点相关联。 参数用于在启动时(和运行时)配置节点,而无需更改代码。 参数的生命周期与节点的生命周期相关(尽管节点可以实现某种持久性以在重新启动后重新加载值)。
参数通过节点名称、节点命名空间、参数名称和参数命名空间进行寻址。 提供参数命名空间是可选的。
每个参数由一个键、一个值和一个描述符组成。 键是字符串,值是以下类型之一:“bool”、“int64”、“float64”、“string”、“byte[]”、“bool[]”、“int64[]”、“float64[]”或“string[]”。 默认情况下,所有描述符都是空的,但可以包含参数描述、值范围、类型信息和其他约束。
Node 声明 Parameters
- 参数设置描述符。 描述符允许您指定参数及其约束的文本描述,例如使其只读、指定范围等
- 构造函数的调用
self.declare_parameter('my_parameter', 'world')创建一个名为my_parameter的参数,默认值为world。 参数类型是从默认值推断出来的,因此在本例中它将被设置为字符串类型
from rcl_interfaces.msg import ParameterDescriptor
my_parameter_descriptor = ParameterDescriptor(description='This parameter is mine!')
import rclpy
import rclpy.node
from rcl_interfaces.msg import ParameterDescriptor
class MinimalParam(rclpy.node.Node):
def __init__(self):
super().__init__('minimal_param_node')
my_parameter_descriptor = ParameterDescriptor(description='This parameter is mine!')
self.declare_parameter('my_parameter', 'world', my_parameter_descriptor)
self.timer = self.create_timer(1, self.timer_callback)
def timer_callback(self):
my_param = self.get_parameter('my_parameter').get_parameter_value().string_value
self.get_logger().info('Hello %s!' % my_param)
my_new_param = rclpy.parameter.Parameter(
'my_parameter',
rclpy.Parameter.Type.STRING,
'world'
)
all_new_parameters = [my_new_param]
self.set_parameters(all_new_parameters)
def main():
rclpy.init()
node = MinimalParam()
rclpy.spin(node)
if __name__ == '__main__':
main()
Parameters 回调
ROS2 Node 可以注册两种不同类型的回调,以便在参数发生变化时收到通知。 这两个回调都是可选的。
- 第一个称为“设置参数”回调,可以通过从节点 API 调用“add_on_set_parameters_callback”来设置。 回调传递一个不可变的“参数”对象列表,并返回“rcl_interfaces/msg/SetParametersResult”。 此回调的主要目的是让用户能够检查即将对参数进行的更改并明确拒绝更改。
重要的是“add_on_set_parameters_callback”回调是多个且没有副作用。其中任意一次回调返回拒绝更新,则不会实际更新参数。其他回调中如果有更改其维护类的相关值,则可能出现与实际参数不一致,所以 ”add_on_set_parameters_callback“ 是 will 将要事件。
第二种回调类型称为“on 参数事件”回调,可以通过从参数客户端 API 调用“on_parameter_event”来设置。 回调传递一个“rcl_interfaces/msg/ParameterEvent”对象,并且不返回任何内容。 在声明、更改或删除输入事件中的所有参数后,将调用此回调。 此回调是 did 完成事件主要目的是让事件消费者可以在确定参数完成更新后,执行对应的处理逻辑
Node 运行时操作 Parameters
ROS 2 中的参数可以通过一组服务获取、设置、列出和描述,如 概念文档 中所述。 ros2 param 命令行工具是这些服务调用的包装器,可轻松从命令行操作参数。
ros2 param list
ros2 param list
此命令将列出给定节点上的所有可用参数,如果没有给定节点,则列出所有可发现节点上的所有可用参数。
要获取给定节点上的所有参数:
ros2 param list /my_node
获取系统中所有节点的所有参数(在复杂的网络上这可能需要很长时间):
ros2 param list
ros2 param get
ros2 param get
此命令将获取特定节点上特定参数的值。
要获取节点上参数的值:
ros2 param get /my_node use_sim_time
ros2 param set
ros2 param set
此命令将设置特定节点上特定参数的值。 对于大多数参数,新值的类型必须与现有类型相同。
要设置节点上参数的值:
ros2 param set /my_node use_sim_time false
命令行上传递的值是 YAML 格式的,因此可以使用任意 YAML 表达式。 但是,这也意味着某些表达式的解释可能与预期不同。 例如,如果节点“my_node”上的参数“my_string”是字符串类型,则以下操作将不起作用:
ros2 param set /my_node my_string off
这是因为 YAML 将“off”解释为布尔值,而“my_string”是字符串类型。 可以通过使用 YAML 语法明确设置字符串来解决此问题,例如:
ros param set /my_node my_string '!!str off'
此外,YAML 支持异构列表,包含(例如)字符串、布尔值和整数。 但是,ROS 2 参数不支持异构列表,因此任何具有多种类型的 YAML 列表都将被解释为字符串。 假设节点“my_node”上的参数“my_int_array”是整数数组类型,则以下操作将不起作用:
ros param set /my_node my_int_array '[foo,off,1]'
以下字符串类型的参数可以起作用:
ros param set /my_node my_string '[foo,off,1]'
ros2 param delete
ros2 param delete
此命令将从特定节点中删除参数。 但请注意,这只能删除动态参数(而不是声明的参数)。 有关更多信息,请参阅:doc:概念文档<../Concepts/Basic/About-Parameters>。
ros2 param delete /my_node my_string
ros2 param describe
ros2 param describe
此命令将提供特定节点上特定参数的文本描述:
ros2 param describe /my_node use_sim_time
ros2 param dump
ros2 param dump
此命令将以 YAML 文件格式打印出特定节点上的所有参数。 然后可以使用此命令的输出稍后使用相同的参数重新运行该节点:
ros2 param dump /my_node
ros2 param load
ros2 param load
此命令将把 YAML 文件中的参数值加载到特定节点中。 也就是说,此命令可以在运行时重新加载由“ros2 param dump”转储出的值:
ros2 param load /my_node my_node.yaml
Node 启动设置 Parameters
所有 ROS 节点都采用一组参数,允许重新配置各种属性。 示例包括配置节点的名称/命名空间、使用的主题/服务名称以及节点上的参数。 所有 ROS 特定参数都必须在“–ros-args”标志后指定:
ros2 run my_package node_executable --ros-args ...
ROS2 Luanch
ROS 2 系统通常由跨许多不同进程(甚至不同机器)运行的许多节点组成。 虽然可以单独运行每个节点,但完整的机器人运行是会启动多个节点运行,且节点之间也可能有依赖关系,如果逐一使用 ros2 run 启动节点运行,那么很快机器人的运行就会变得很麻烦。
ROS 2 中的启动系统旨在使用单个命令自动运行许多节点。 它帮助用户描述其系统的配置,然后按照描述执行。 系统的配置包括要运行哪些程序、在哪里运行它们、向它们传递哪些参数以及 ROS 特定的约定,通过为每个组件提供不同的配置,可以轻松地在整个系统中重用组件。 它还负责监视已启动进程的状态,并报告和/或对这些进程状态的变化做出反应。
以上所有内容都在启动文件中指定,该文件可以用 Python、XML 或 YAML 编写。 然后可以使用“ros2 launch”命令运行此启动文件,并运行所有指定的节点。
- 系统详细设文档: design.ros2.org/articles/ro…
- 创建启动文件: www.ncnynl.com/ros2docs/cn…
python 主要类 LaunchDescription 和 Node:
from launch import LaunchDescription
from launch_ros.actions import Node
Node
Node 负责配置一个节点的相关信息,主要字段:
- package='turtlesim' : 节点包名
- namespace='turtlesim1' : 命名空间,同一个节点在使用不同命名空,则允许系统启动两个节点实例,而不会发生节点名称或主题名称冲突
- executable='turtlesim_node' : 节点执行程序
- name='sim'
- remappings : 举个例子说,假如node原本订阅了topic_A,那么需要向topic_B发布话题才能被node接收到;假如node原本向topic_A发布消息,那消息实际将被发布到topic_B上。
IncludeLaunchDescription
在 ROS 2 的 launch 文件中,可以使用 IncludeLaunchDescription 来包含并启动其他 launch 文件。
例如要在你的 launch.py 文件中增加对 slam_toolbox 的 online_sync_launch.py 的启动,可以按照以下方式操作:
from launch import LaunchDescription
from launch.actions import IncludeLaunchDescription
from launch.launch_description_sources import PythonLaunchDescriptionSource
from ament_index_python.packages import get_package_share_directory
import os
def generate_launch_description():
# 获取 slam_toolbox 包的路径
slam_toolbox_dir = get_package_share_directory('slam_toolbox')
# 构建 online_sync_launch.py 的完整路径
online_sync_launch_path = os.path.join(
slam_toolbox_dir,
'launch',
'online_sync_launch.py'
)
# 包含并启动 online_sync_launch.py
slam_toolbox_launch = IncludeLaunchDescription(
PythonLaunchDescriptionSource(online_sync_launch_path)
)
return LaunchDescription([
slam_toolbox_launch,
# 你可以在这里添加其他节点或 launch 文件
])
如果你需要传递参数(如 params_file 或 use_sim_time),可以这样做:
slam_toolbox_launch = IncludeLaunchDescription(
PythonLaunchDescriptionSource(online_sync_launch_path),
launch_arguments={
'use_sim_time': 'true',
'slam_params_file': '/path/to/your/slam_params.yaml'
}.items()
)
执行启动文件
ros2 launch <package_name> <launch_file_name>
对于带有启动文件的软件包,最好在软件包的“package.xml”中添加对“ros2launch”包的“exec_depend”依赖项:
<exec_depend>ros2launch</exec_depend>