ROS 提高篇 之 Launch 深入研究 - 01 — 启动文件的编程 — ROS 的 XML语法简介
我使用的虚拟机软件:VMware Workstation 11
使用的Ubuntu系统:Ubuntu 14.04.4 LTS
ROS 版本:ROS Indigo
注意:
1 . ROS 提高篇这个专栏的教学有门槛。
2 . 如果你没有学习前面的教程,请想学习前面的 beginner_Tutorials 和 learning_tf 的ROS 相关教程。
1 . 前言
在之前的 beginner_Tutorials 教程里面,我们已经初步学习了 Launch文件 要如何编写。但是那篇博客只是简单的讲解了一些 Launch 文件里面比较常用的几个 标签如何使用。
这一篇博客和接下来的几篇博客,我们将详细的讲解 Launch 文件要如何编写。
使用 launch文件,我们可以一次性配置并且运行很多节点。
至今,如果你按照我们的教程(ROS Learning)学习到这里,想必你一定运行了许多的例子。你可能会对在许多的终端中运行不同的节点感到麻烦,并且你会因为不了解我们运行过的例子中的 launch文件 都做了什么事情会感到困扰。我们在ROS Learning教程中 的 beginner_Tutorials 和 learning_tf 的ROS 相关教程 中已经学习简单的 launch启动脚本 如何编写,现在的你所知道的launch文件的编写技巧已经无法满足你现在想编写更复杂更大型的launch启动脚本文件了。所以这一篇博客和接下来的几篇博客,我们会详细的讲 launch文件 的技巧,当你学完这几篇的博客,你会具备编写大型launch启动脚本的能力。
说了一堆废话,接下来进入正题。
2 . 使用launch启动脚本文件:
<launch>
<node
pkg="turtlesim"
type="turtlesim_node "
name="turtlesim "
respawn="true "
/>
<node
pkg="turtlesim"
type="turtle_teleop_key "
name="teleop_key"
required="true"
launch−prefix="xterm −e"
/>
<node
pkg="agitr"
type="subpose"
name="pose_subscriber"
output="screen"
/>
</launch>
roslaunch 是如何允许我们一次启动多个节点”?
基本的实现思想是列表,在一个指定的XML格式,一组节点需要在同一时间被启动。你会很熟悉下面的列表,它展示启动了海龟(turtlesim)模拟器的一个简单的launch文件。
再我们深入研究launch文件的格式的细节前,让我们看看这些文件都是如何使用的:
roslaunch
命令的使用格式:
# roslaunch package-name launch-file-name
在 编写简单的启动脚本文件 的博客中最后的小练习中,我们自己编写了一个launch文件,如果一切工作正常的话,这个命令将会启动3个节点。在开始任何一个节点前,roslaunch
将会确定 roscore节点 是否已经在运行,如果没有,自动启动它。
注意: 要小心,不要与
rosrun
命令 混淆,rosrun
只能启动一个节点。roslaunch
命令可以同时启动多个节点。
扩展 :你也可以在使用 roslaunch
启动launch文件的时候没有package-name
这部分。要想做到这一定,launch-file-name
文件需要写成绝对路径的形式。只要可以让 roalaunch
命令找到唯一的一个 launch
文件,你就可以不提供 package-name
部分。举例:
$ roslaunch ~/catkin_ws/src/beginner_turtorls/launch/xxx.launch
关于
roslaunch
的一个重要的事实,这个事实很容易被忽略,就是在launch文件中的所有节点几乎被同一时间启动。结果,我们不能确定这些节点初始化的顺序。好在 ROS节点 不关系彼此的启动顺序。为什么?
这个行为体现了ROS哲学:每一个节点与其他的节点都应该尽可能的独立、不相关。
如果你想请求 roslaunch
命令 请求详细输出( Requesting verbosity )信息的话。就像其他的命令行工具一样,roslaunch
也有请求详细输出的选项。在你进行调试程序的时候,使用 -v
参数对你是有帮助的,你可以看到 roslaunch
是如何解读你的 launch文件 的详细解释。
# roslaunch -v package-name launch-file-name
3 . 创建launch启动脚本文件:
3-1 . 基本的成分:
3-1-1 . <launch>
最简单的启动文件是由一个包含了几个节点元素的根元素(root element)组成。
根元素:launch文件是XML文档,每一个XML文档必须有且只有一个跟元素(root element)。对于ROS launch文件,根元素(root element)是由一对 launch
标签定义的。
<launch>
...
</launch>
其他的所有元素标签都要写在 <launch>...</launch>
便签之间。
3-1-2 . <node>
启动的节点:任何一个launch文件的重点都是:节点(node)元素的集合。启动的每一个节点(node)都要有自己独一无二的名字(name)。一个节点(node)元素看上去就像下面这个样子:
<node
pkg="package-name"
type="executable-name"
name="node-name"
/>
注意: 在节点(node)标签的末尾加上’ /
‘斜杠是重要的,并且也容易被忘记。’ /
’ 表明:这里不会再有”</node>
“出现,并且node元素定义完成。
XML解析器非常严格,如果你忘记了加’ /
‘,你要对这样的错误做好准备:
Invalid roslaunch XML syntax: mismatched tag
你也可以写成显式关闭标签的形式:
<node pkg=". . . " type=". . . " name=". . . "></node>
事实上,如果node(节点)元素有children,就需要显式标签来定义,children比如是:remap
元素或者 param
元素。这些元素会在下面介绍。
3-1-2-1 . 一个node(节点)元素必需的三个属性:pkg
、 type
和 name
属性:
pkg
和 type
属性确定了:启动此节点,ROS应该运行哪个程序。它们与 rosrun
的两个命令行参数一样,它们分别是:程序包名字 和 可执行文件的名字。
name
属性指定了一个节点的名字。这个节点程序会调用 ros::init()
函数内有一个这个节点的 name
参数,导致这个节点的 name
被改写,所以这个 name
属性容易被覆盖。
扩展:
ros::init()
函数 提供的name
信息将会全面的覆盖命名信息(launch文件中<node>
标签里面的name
属性),就算这个节点已经以匿名name被创建。如果你想在 launch文件 中使用匿名name,使用anon
属性 来替代name
属性:
name="$(anon base_name)"
但是请注意,多次使用同样的基本名称,将生成同样的匿名name。这意味着,(一)我们参考launch文件中其他部分的name,但(二)我们必须小心的为每一个我们想要匿名的节点使用不同的base names。
3-1-2-2 . 查找节点(node)的日志文件(log files):
使用 roslaunch
命令 和 使用 rosrun
命令 单独运行每个节点 之间的重要区别是:默认情况下,roslaunch
命令 从启动节点开始,标准输出信息会重定向到一个日志文件中,而不会像 rosrun
命令那样,将 log 信息显示在终端(console)上。
日志文件所在路径: ∼/.ros/log/run_id/node_name-number-stdout.log
当主机( master )被启动,run_id
是被生成的唯一的标识符。在log文件名中的number是一个小整数,表示节点的数量。例如:运行上面的launch文件,2个节点的标准输出信息将会传给这些节点的日志文件(log files):
turtlesim-1-stdout.log
telep_key-3-stdout.log
这些日志文件可以被任何的文本编辑器(比如:gedit、vim、cat)打开。
3-1-2-3 . 将标准输出信息显示在终端(console)上
Q: 如何将标准输出信息显示在终端(console)上?
A: 在 node
元素中使用 output
属性:
output="screen"
带这个属性启动的节点会将标准输出信息显示在终端的窗口中,而不会保存在日志文件中。这也解释了为什么这个带有output="screen"
的节点(node
) 的日志文件在上面日志文件列表中丢失的原因。
扩展:
node
元素的output
属性只能影响这个节点自己。除了output
属性,我们可以使用roslaunch
命令行工具的--screen
命令行选项强制性的在终端的窗口中显示所有节点的输出信息。
roslaunch --screen package-name launch-file-name
3-1-2-4 . 请求重生(respawning) :
启动完所有请求启动的节点之后,roslaunch
监测每一个节点,让它们保持正常的运行状态。对于每一个节点(node
),当它终止( terminates)时,我们可以要求 roslaunch
重新启动它,这就是 respawn
属性做的事情:
respawn="true"
-- This can be useful, for example, for nodes that might terminate prematurely, due to software crashes, hardware problems, or other reasons.
The respawn attribute is not really necessary in our example—All three programs are
quite reliable—but we include it for the turtlesim_node to illustrate how respawning
works. If you close the turtlesim window, the corresponding node will terminate. ROS
quickly notices this and, since that node is marked as a respawn node, a new turtlesim
node, with its accompanying window, appears to replace the previous one.
3-1-2-5 . 必需节点(Requiring):
让一个节点可以重生的另一种方法是:宣布这个节点是必需的(require
):
required="true"
当一个必需的节点终止时,roslaunch
会做出响应,终止其他所有的节点并退出它自己。这种行为机制是有用的。例如,对于一些节点(一)它们非常的重要,如果它们失败了它,那么整个会话应该被放弃,或者这些节点(二)不能使用重生( respawn
)属性完美的重新启动,在这些情况下,我们都需要对这个 节点(node
)使用 required="true"
属性。上面 roslaunch
例子文件中,我们给 turtle_teleop_key
节点使用了这个 required
属性。如果你关闭了运行 teleoperation
节点的窗口,roslaunch
将会杀死其他的2个节点退出。
注意: 由于
required
属性和respawn
属性的含义,所以如果你给单个的一个节点同时设置了这2个属性,roslaunch
命令会抱怨。所以不要这样设置。
3-1-2-6 . 让每一个节点在单独的终端窗口中启动它们自己:
使用 roslaunch
命令 的一个潜在的缺点:相比我们原来对每个节点在单独的终端使用 rosrun
命令启动的做法,roslaunch
则是让所有的节点共享同一个终端。 那些只需要生产简单的日志消息文件而不需要终端(console)输入的节点是容易管理的,而那些依赖终端输入的节点,比如 turtle_teleop_key
节点,它可能要优先的保留在独立的终端上。
庆幸的是,roslaunch
提供了一个简单的属性去实现这一点,在 node
元素里使用 launch-prefix
属性:
launch-prefix="command-prefix"
在例子launch文件中,我们给 teleoperation
节点使用了这个属性:
launch-prefix="xterm -e"
因为这个属性,启动这个 node
元素的 rosrun
命令大致相当于:
xterm -e rosrun turtlesim turtle_teleop_key
正如我们所知道的,xterm
命令会开一个新的终端窗口。 -e
参数告诉 xterm
:执行其命令行剩余部分(rosrun turtlesim turtle_teleop_key
)。
扩展:
launch-prefix
属性不是只能使用xterm
。它还可以使用gdb
或者valgrind
或者nice
命令。
3-1-2-7 . 在一个命名空间内启动一个节点:
<launch>
<node
name="turtlesim_node "
pkg="turtlesim"
type="turtlesim_node "
ns="sim1"
/>
<node
pkg="turtlesim"
type="turtle_teleop_key "
name="teleop_key"
required="true"
launch −prefix="xterm −e"
ns="sim1"
/>
<node
name="turtlesim_node "
pkg="turtlesim"
type="turtlesim_node "
ns="sim2"
/>
<node
pkg="a gitr "
type="pubvel"
name="velocity_publisher "
ns="sim2"
/>
</launch>
在 node
元素中指定 ns
属性:
ns="namespace"
在例子launch文件中,使用这个属性创建了两个无关的 turtlesim 模拟器。
turtlesim 话题名字(turtle1/cmd_vel
、turtle1/color_sensor
和 turtle1/pose
)被从全局命名空间移动到 /sim1
和 /sim2
的单独命名空间里。
3-1-2-8 . 重映射名字:
在启动一个节点的时候,有两种方法创建重映射:
在终端命令行中启动一个节点时,要重新给这个节点命名:给出一个节点原来的名字和新的名字,中间用:=分开。
original-name:=new-name
例如,在运行turtlesim实例时,我们现在想把发布姿态数据的话题/turtle1/pose
名称改为:/tim
,那么命令就是这样的:
rosrun turtlesim turtlesim_node turtle1/pose:=tim
在launch文件中重新命名:使用 remap
元素:
<remap from="original-name" to="new-name" />
如果这个 remap
是 launch
元素的一个child(子类),与 node
元素同一层级, 并在 launch
元素内的最顶层。那么这个 remapping 将会作用于后续所有的节点。
这个 remap
元素也可以作为 node
元素的一个child(子类)出现。下面这个就是使用模板:
<node node-attributes >
<remap from="original-name" to="new-name" />
. . .
</node>
例如:上面命令行命令如果在launch文件中,就是下面这个样子的:
<node pkg="turtlesim" type="turtlesim_node"
name="turtlesim" >
<remap from="turtle1/pose" to="tim" />
</node>
3-2 . 其他launch文件元素:
这一节介绍几个额外的 roslaunch
结构元素。
3-2-1 . <include>
元素
3-2-1-1 . file
属性
包含(including)其他文件:
<include file="path-to-launch-file" />
这个 file
属性期望我们添加想要包含的文件的完整路径。但是大多数时候,include
元素使用一个 find
命令来搜索一个程序包,代替一个明确的完整路径:
<include file="$(find package-name)/launch-file-name" />
<launch>
<node
pkg="turtlesim"
type="turtlesim_node "
name="turtlesim "
>
<remap
from="turtle1 /cmd_vel"
to="turtle1 /cmd_vel_reversed"
/>
</node>
<node
pkg="turtlesim"
type="turtle_teleop_key "
name="teleop_key"
launch−prefix="xterm −e"
/>
<node
pkg="a gitr "
type="reverse_cmd_vel"
name="reverse_velocity "
/>
</launch>
注意:roslaunch
命令 将会在程序包(package)的子目录里搜索launch文件。 include
元素必须要指定文件的特定路径,你可以使用 find
来找到这个程序包,但是却不能在这个程序包目录里面自动的找到某个子目录里有launch文件。举例:
这样做是正确的:<include file = "find learning_tutrols"/launch/start_demo.launch" / >
这样做是错误的:<include file = "find learning_tutrols"/start_demo.launch" />
你可以使用 roslaunch
命令:roslaunch learning_tutrols start_demo.launch
,这样是可以成功运行的。但是同样的程序包名和launch文件名使用include
元素就会失败。
3-2-1-2 . ns
属性
include
元素也支持 ns
属性,可以让这个文件里的内容推送到一个命名空间里面:
<include file=". . . " ns="namespace" />
一般我们都会给 include
元素设置一个 ns
属性。
3-2-2 . 启动参数(Launch arguments)
为了帮助launch文件配置,roslaunch
支持 launch arguments(启动参数),也叫做 arguments
或者 args
。它的功能有点像一个可执行程序的局部变量。它的优点是,你可以通过编写launch文件来避免编写重复代码。为少数的信息使用arguements,可以改变程序的运行。
为了说明这个道理,我们的例子launch文件中使用了一个argument
,叫做 use_sim3
,目的是为了确定是否启动了3个turtlesim副本或只有2个。
Z:尽管术语argument和parameter在许多计算机环境中稍微可以互换使用,它们的含义在ROS中有很大的不同。Parameters(参数)在一个运行的ROS系统中是变量(values),它被存储在parameter服务器中,活动(或者叫:运行)的节点通过
ros::param::get()
函数访问它,并且用户可以通过rosparam
命令行工具使用它。相比之下,arguments只有在launch文件里合法,它们的值不是直接提供给节点。
<launch>
<include file ="$( find agitr )/doublesim.launch "/>
<arg
name="use_sim3"
default ="0"
/>
<group ns="sim3" if ="$( arg use_sim3 )" >
<node
name="turtlesim_node "
pkg="turtlesim"
type="turtlesim_node "
/>
<node
pkg="turtlesim"
type="turtle_teleop_key "
name="teleop_key"
required="true "
launch−prefix="xterm −e"
/>
</group>
</launch>
3-2-3 . Declaring arguments(声明参数)
在launch文件中,要声明一个参数的存在,我们使用arg
元素:
<arg name="arg-name" />
声明里面只有一个 name
是起不上什么作用的,这就像是你在程序中定义了一个 int
类型的变量,但是你并没有使用它一样。(你至少还需要给 arg
元素分配 default
属性或 value
属性,请看下面)
3-2-3-1 . Assigning argument values(分配argument的数值)
在launch文件中使用的每一个argument必须给它分配一个 value(赋值)。有这么一下几种方法来实现这一点:
在命令行中你可以这样做:给 roslaunch
提供一个 value
:
roslaunch package-name launch-file-name arg-name:=arg-value
另外,在launch文件中,你可以提供一个 value(赋值) 作为 arg
声明的一部分,使用下面的两种语法之一就可以:
<arg name="arg-name" default="arg-value" />
<arg name="arg-name" value="arg-value" />
这两种语法的唯一不同是:命令行是argument可以覆盖default
的值,但是不能覆盖 value
。在例子launch文件中,use_sim3
节点的 default
值为 0
,所以它可以通过命令行改变值,就像下面这样:
roslaunch agitr triplesim.launch use_sim3:=1
如果我们修改了这个例子launch文件:使用value
替换default
。那么上面这个命令执行的时候会出现错误,因为使用 value
属性 配置的argument的值是不允许改变的。
3-2-3-2 . Accessing argument values(获取argument的数值)
一旦声明了一个argument,并且使用 value
属性给它分配值,你可以在程序中通过arg substitution来使用它的 value
属性的值。像是这样子:
$(arg arg-name)
$()
这个符号出现的任何地方,roslaunch
命令 都将会把它替换成给定argument 的值(value
)。在例子launch文件中,我们使用 use_sim3
参数(argurnt),???????一个 group
元素内的 if
属性。(下面我们将会简短的介绍 if
和 group
)
3-2-4 . Sending argument values to included launch files(给包含(included)的launch文件传递argument)
在argument的传递上有一个限制,就是argument不能传递给 include
元素里包含的子launch文件使用。这个问题非常重要,因为这个 argument 就像是一个局部变量,它不能被包含的launch文件所 “继承” 。
解决这个问题的方法:在 include
元素中插入 arg
元素作为 include
元素的子类(children),就像是这样:
<include file="path-to-launch-file">
<arg name="arg-name" value="arg-value"/>
. . .
</include>
注意,这里的 arg
元素不同于我们已经知道的 arg
声明,在 inchude
标签内的arguments是给包含(included) 的launch文件提供的arguments,不是为本launch文件提供的。
一种常见的情况是,被包含(included)的launch文件和本launch文件会有共同的参数。在这种情况下,我们希望这些值(values)永远不变。像这样的元素,在这两个地方使用相同的argument name(参数名),要这样做:
<arg name="arg-name" value="$(arg arg-name)" />
在这种情况下,第一个 arg-name
和往常一样。第二个 arg-name
是launch文件中提供的。结果是,这两个launch文件中给定的argument具有相同的值(value)。
3-2-5 . Creating groups (创建组)
最后一个要介绍的launch文件特性就是,再来说说:group
元素,它提供了一个方便的方法来在一个大型的launch文件中组织节点。group
元素可以达到两个目的:
1 . 组(group)可以把几个节点推送到同一个命名空间中。
<group ns="namespace" />
. . .
</group>
2 . 组(groups)里面的每一个节点在启动的时候都会给定默认的命名空间。
:如果一个已经被分组的节点有自己的
ns
属性,那么结果:被启动的节点的命名空间会出现嵌套的关系:/group-namespace/ns-namespace/node-name
。
3 . 组(groups)可以通过 判别条件 来启用或禁用节点(nodes):
<group if="0-or-1" />
. . .
</group>
如果 if
属性的值是 1
,<group>
标签内封闭的元素(elements)会被包含。如果 if
属性 值是 0
,则 <group>
标签内包含的元素会被忽略。 unless
属性的工作方式类似 if
属性,但是含义颠倒:
<group unless="1-or-0" />
. . .
</group>
当然了,通常我们不会给这些属性使用简单的 0
或 1
这样的幅值。建议:结合 arg
的 $()
技术,它们会将你的launch文件的配置变得非常的强大。
注意: 注意
1
和0
是唯一的合法值。具体来说,你通常会使用布尔(boolean)的 AND 和 OR 操作符,而不会直接使用1
或0
。
在例子launch文件中,有一个group
元素具有ns
和 if
这两属性。这个 group
元素的 ns
属性:将这个 group
元素内的两个节点推送到 sim3
命名空间。if
属性:基于use_sim3
参数的值来落实:是启动还是禁止 group
元素内包含的节点。
扩展: 遗憾的是,只有这三个属性(
ns
、if
、unless
)可以通过group
元素向下传递给group
元素内包含的node
(节点)元素。
例如,我们可能会想给group
元素添加一个output="screen"
来让这个group
元素内的节点启动后输出的信息都显示在终端窗口里,但是我们不可以这样设置,我们必须要对每一个node
元素直接给予output="screen"
,这样才能达到我们的要求。
完
By The Way: 可能以后,这篇博客,我还会继续完善。现在的这些关于:如何编写Launch文件的语法 ,基本上介绍的差不多了。