在ROS系统中,无论话题还是服务,或者我们后续将要学习的动作,都会用到一个重要的概念——通信接口。
通信并不是一个人自言自语,而是两个甚至更多个人,你来我往的交流,交流的内容是什么呢?为了让大家都好理解,我们可以给传递的数据定义一个标准的结构,这就是通信接口。
接口的定义
接口的概念在各个领域随处可见,无论是硬件结构还是软件开发,都有广泛的应用。
比如生活中最为常见的插头和插座,两者必须匹配才能使用,电脑和手机上的USB接口也是,什么Micro-USB、TypeC等等,都是关于接口的具体定义。
软件开发中,接口的使用就更多了,比如我们在编写程序时,使用的函数和函数的输入输出也称之为接口,每一次调用函数的时候,就像是把主程序和调用函数通过这个接口连接到一起,系统才能正常工作。
更为形象的是图形化编程中使用的程序模块,每一个模块都有固定的结构和形状,只有两个模块相互匹配,才能在一起工作,这就很好的讲代码形象化了。
所以什么是接口,它是一种相互关系,只有彼此匹配,才能建立连接。
回到ROS的通信系统,它的主要目的就是传输数据,那就得让大家高效的建立连接,并且准确包装和解析传输的数据内容,话题、服务等机制也就诞生了,他们传输的数据,都要符合通信接口的标准定义。
比如摄像头驱动发布的图像话题,由每个像素点的R、G、B三原色值组成,控制机器人运动的速度指令,由线速度和角速度组成,进行机器人配置的服务,有配置的参数和反馈的结果组成等等,类似这些常用的定义,在ROS系统中都有提供,我们也可以自己开发。
这些接口看上去像是给我们加了一些约束,但却是ROS系统的精髓所在。举个例子,我们使用相机驱动节点的时候,完全不用关注它是如何驱动相机的,只要一句话运行,我们就可以知道发布出来的图像数据是什么样的了,直接开始我们的应用开发;类似的,键盘控制我们也可以安装一个ROS包,如何实现的呢?不用关心,反正它发布出来的肯定是线速度和角速度。
ROS通信接口 接口可以让程序之间的依赖降低,便于我们使用别人的代码,也方便别人使用我们的代码,这就是ROS的核心目标,减少重复造轮子。
ROS有三种常用的通信机制,分别是话题、服务、动作,通过每一种通信种定义的接口,各种节点才能有机的联系到一起。
为了保证每一个节点可以使用不同语言编程,ROS将这些接口的设计做成了和语言无关的,比如这里看到的int32表示32位的整型数,int64表示64位的整型数,bool表示布尔值,还可以定义数组、结构体,这些定义在编译过程中,会自动生成对应到C++、Python等语言里的数据结构。
话题通信接口的定义使用的是.msg文件,由于是单向传输,只需要描述传输的每一帧数据是什么就行,比如在这个定义里,会传输两个32位的整型数,x、y,我们可以用来传输二维坐标的数值。
服务通信接口的定义使用的是.srv文件,包含请求和应答两部分定义,通过中间的“—”区分,比如之前我们学习的加法求和功能,请求数据是两个64位整型数a和b,应答是求和的结果sum。
动作是另外一种通信机制,用来描述机器人的一个运动过程,使用.action文件定义,比如我们让小海龟转90度,一边转一边周期反馈当前的状态,此时接口的定义分成了三个部分,分别是动作的目标,比如是开始运动,运动的结果,最终旋转的90度是否完成,还有一个周期反馈,比如每隔1s反馈一下当前转到第10度、20度还是30度了,让我们知道运动的进度。
大家可能好奇ROS系统到底给我们定义了哪些接口呢?我们可以在ROS安装路径中的share文件夹中找到,涵盖众多标准定义,大家可以打开几个看看。
ROS2通过自带rosidl_default_generators包来为自定义的msg、srv和action文件生成各个语言的头文件!
ROS2通过在CMakeLists.txt中调用新增的宏rosidl_generate_interfaces来为msg、srv和action文件生成各个语言的头文件!而这个宏的实现定义在rosidl_default_generators包。
利用ROS2的原始基本数据类型和已有的数据类型,可以自定义需要的数据类型。
2.1 ROS2的基本原始数据类型
ROS2目前支持的内置类型:
每种内置类型都可以用来定义数组:
所有比其ROS定义更宽松的类型均由软件强制执行ROS的范围和长度约束。
使用数组和有界类型的消息定义示例:
int32[] unbounded_integer_array
int32[5] five_integers_array
int32[<=5] up_to_five_integers_array
string string_of_unbounded_size
string<=10 up_to_ten_characters_string
string[<=5] up_to_five_unbounded_strings
string<=10[] unbounded_array_of_string_up_to_ten_characters each
string<=10[<=5] up_to_five_strings_up_to_ten_characters_each
在本文中,将在自己的包中创建自定义的.msg、.srv和.action文件,然后在另外的包中使用它们,这两个包应该在同一个工作空间dev_ws/src目录中,然后运行以下命令创建一个新包。
ros2 pkg create --build-type ament_cmake learning_inteface
tutorial_interfaces是新包的名称。注意,它是一个CMake包,目前还没有办法在纯Python包中生成.msg、.srv和.action文件。但是,可以在CMake包中创建自定义接口,然后在Python节点中使用它。
将.msg、.srv和.action文件保存在自己的包中是一种很好的做法。在dev_ws/src/learning_inteface 目录下创建:
mkdir msg srv action
3.2.1 自定义msg文件
在learning_inteface/msg文件夹下,创建一个名为Num.msg的新文件,用一行代码声明它的数据结构:
int64 num
这是自定义的消息,它传输一个名称为num的64位整数。
3.2.2 自定义srv文件
srv文件和msg文件相似,除了它包含两个部分:请求和响应。这两部分用“—”线分隔。
在learning_inteface/srv文件夹下,创建一个名为AddThreeInts.srv的新文件,并声明他的请求和响应结构:
int64 a
int64 b
---
int64 sum
这是自定义的服务,它请求三个名为a、b 的整数,并以一个名为sum的整数进行响应。
3.2.3 自定义action文件
按照以下格式定义.action文件:
Request
---
Result
---
Feedback
动作定义由三个消息定义组成,它们之间用–分隔。
请求(request)消息从动作客户端发送到动作服务器,初始化一个新目标。
当目标完成时,结果(result)消息从动作服务器发送到动作客户端。
反馈(feedback)消息定期从动作服务器发送到动作客户端,其中包含关于目标的更新。
一个动作的实例通常被称为目标(goal)。
假设想要定义一个新的动作“Fibonacci”来计算Fibonacci序列。
在learning_inteface/action目录中,创建一个名为Fibonacci.action的文件,内容如下:
int32 order
---
int32[] sequence
---
int32[] partial_sequence
目标请求是想要计算的斐波那契数列的order,结果是最终的sequence,而反馈是到目前为止计算的partial_sequence。
要将自定义的接口(interface)转换成基于语言的代码(如C++和Python),以便它们可以在这些语言中使用,请将以下代码添加到CMakeLists.txt中:
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"msg/Num.msg"
"srv/AddThreeInts.srv"
"action/Fibonacci.action"
)
注意:需要依赖于action_msgs,因为动作定义包括额外的元数据(例如目标id)。
因为接口依赖于rosidl_default_generators来生成基于语言的代码,所以需要声明对它的依赖关系。提供其他包使用,将以下行添加到package.xml中:
<depend>action_msgs</depend>
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
3.3.3 编译
在工作空间的根目录(~/dev_ws)中,运行以下命令:
colcon build --packages-select learning_inteface
source install/setup.bash
现在接口将被其他ROS 2包发现。