ROS工作空间简介
元功能包:功能包的组织者
对于ROS中的文件结构,我们重点关注src部分,我们经过了解ROS的文件结构知道:src目录下可以包含多个功能包,假设我们需要使用机器人导航模块,但是这个模块中包含着地图、定位、路径规划...等不同的功能包,它们的逻辑关系如下:
如果我要获取“导航功能”,我需要一个一个按照功能包之间的依赖关系启动功能包,这样有些太麻烦了。在Linux系统中为了更方便的组织工程项目(这里针对的是项目文件,即功能包),出现了“元功能包”的概念。这个是一个“虚包”,就是这个功能包的src目录下没有源文件,因此自身不会实现专属功能,其功能的实现完全依赖于其他的功能包,起到一个组织功能包的作用。
我们以导航模块中的元功能包为例:
上述图片中显示了导航模块的所有功能包,其中navigation功能包为元功能包(metapackage),元功能包中由于没有src目录因此无需添加任何依赖项,因为这个功能包没有自己的专属功能,它的功能是借助其他的功能包的功能来实现的。元功能包有两个文件即可:
一个是package.xml文件:用于声明元功能包所依赖的其他功能包;另一个是CMakelist.txt文件:用于指定功能包之间的依赖关系。
CMakelist.txt(其他无用部分一定要删除,以防报错):
cmake_minimum_required(VERSION 3.0.2)
project(navigation)
find_package(catkin REQUIRED)
catkin_metapackage() // 只需添加此条内容即可
package.xml:
<exec_depend>amcl</exec_depend>
<exec_depend>base_local_planner</exec_depend>
<exec_depend>carrot_planner</exec_depend>
<exec_depend>clear_costmap_recovery</exec_depend>
<exec_depend>costmap_2d</exec_depend>
<exec_depend>dwa_local_planner</exec_depend>
<exec_depend>fake_localization</exec_depend>
<exec_depend>global_planner</exec_depend>
<exec_depend>map_server</exec_depend>
<exec_depend>move_base</exec_depend>
<exec_depend>move_base_msgs</exec_depend>
<exec_depend>move_slow_and_clear</exec_depend>
<exec_depend>navfn</exec_depend>
<exec_depend>nav_core</exec_depend>
<exec_depend>rotate_recovery</exec_depend>
<exec_depend>voxel_grid</exec_depend>
<export>
<metapackage/> // 表征:这个功能包为元功能包
</export>
其中,最后<export>…</export>表明这个功能包为元功能包。
Launch文件:源文件的组织者
那说完了如何组织功能包,我们谈谈如何组织一个功能包内的众多源文件。如果我们一个节点一个节点的启动,这样耗时耗力。Launch可以帮助我们解决这个难题,下面解释以下launch文件的编写(以ROS系统自带的turtle节点为例):
① 节点启动标签
<launch>
<node pkg = "turtlesim" type = "turtlesim_node" name = "my_node"/>
<node pkg = "turtlesim" type = "turtle_teleop_key" name = "my_key"/>
</launch>
注意:因为ROS中采用多线程,因此节点的运行不会按照节点在launch中排列顺序进行。
在launch文件中,都是如下形式:
<launch>
...
</launch>
<launch>是launch文件根标签,文件中所有标签的编写都应该在<launch>…</launch>之间。千万要记住一点:roslaunch package_name launch_file_name.launch之前一定要保存launch文件,否则你编写的launch不会起作用而且roslaunch命令会自动启动roscore无需我们操作,而且运行roslaunch命令无需我们反复的编译我们的ROS工程项目。其实launch标签也有一个子级标签deprecated,用于文本说明:
<launch deprecated="this vision is out-of-date!">
</launch>
结果如下:
其实,我们如果认为给很多节点取名太麻烦,可以使用name=”$(anon node_name)”标签在节点node_name名称之后加一些随机数,使得该节点名称在整个catkin编译项目中唯一:
<launch deprecated="this vision is out-of-date!">
<!-- the topic of turtlesim_node is /turtle1/cmd_vel -->
<node pkg="turtlesim" type="turtlesim_node" name="$(anon my_node)"/>
<!-- the topic of turtle_teleop_key is /turtle1/cmd_vel -->
<node pkg="turtlesim" type="turtle_teleop_key" name="my_key" output="screen"/>
</launch>
输出结果如下:
其实几乎所有的标签都可以作为<node>的子级标签:
1. 意外关闭后自动启动的子级标签
<launch>
<node pkg="turtlesim" type="turtlesim_node" name="my_node" respawn="true"/>
<node pkg="turtlesim" type="turtle_teleop_key" name="my_key" respawn="true"/>
</launch>
Respawn=true|false表示:如果节点意外关闭是否重新启动。
2. 在指定机器上启动节点的子级标签
我们知道ROS是一个分布式系统,也就是说它的各个部分可以分布在不同的机器上,我们要在一个机器上远程启动另一个机器上的节点可以用这个标签来实现。首先,我们先要明确几个参数:
1)name="machine-name":给机器取得名字;
2)address="blah.willowgarage.com":网络地址/机器的主机名;
3)env-loader="/opt/ros/fuerte/env.sh":指定远程机器上的环境文件。环境文件必须是一个shell脚本,它设置所有必需的环境变量,然后对提供的参数运行exec。有关示例文件,请参阅ROS Fuerte的debian安装时提供的env.sh;
4)default="true|false|never" (optional):Sets this machine as the default to assign nodes to. The default setting only applies to nodes defined later in the same scope. NOTE: if there are no default machines, the local machine is used. You can prevent a machine from being chosen by setting default="never", in which case the machine can only be explicitly assigned;
5)user="username" (optional):登录机器的SSH用户名;
6)password="passwhat"(strongly discouraged):SSH密码。强烈建议您配置SSH密钥和SSH代理,以便您可以使用证书代替登录;
7)timeout="10.0" (optional):远程启动节点最长允许的时间。
示例如下:(先配置machine信息,在使用<node machine=” machine-name”…/>启动节点)
<launch>
<machine name="foo" address="foo-address" env-loader="/opt/ros/fuerte/env.sh" user="someone"/>
<node machine="foo" name="footalker" pkg="test_ros" type="talker.py" />
</launch>
详细请见:roslaunch/XML/machine - ROS Wikihttp://wiki.ros.org/roslaunch/XML/machine
3. 节点延时启动的子级标签
<launch>
<node pkg="turtlesim" type="turtlesim_node" name="my_node" respawn_delay="10"/>
<node pkg="turtlesim" type="turtle_teleop_key" name="my_key" respawn_delay="10"/>
</launch>
Respawn_delay=”delay_time”:表示节点延迟启动delay_time秒。
4. “如果XXX节点结束运行(XXX节点被杀死),则所有节点都停止运行”的子级标签
<launch>
<node pkg="turtlesim" type="turtlesim_node" name="my_node" required="true"/>
<node pkg="turtlesim" type="turtle_teleop_key" name="my_key" />
</launch>
Required=true|false一般用在非常重要的节点(一旦这个节点停止运行则整个系统都会受影响)上。一旦这个节点进程结束(这个节点被杀死了),整个系统的所有节点都会停止运行。
5. 给节点名称添加前缀(给节点添加命名空间)的子级标签
<launch>
<node pkg="turtlesim" type="turtlesim_node" name="my_node" ns="hello"/>
<node pkg="turtlesim" type="turtle_teleop_key" name="my_key"/>
</launch>
运行launch文件之后,我们用rosnode list会发现:
乌龟节点的话题从/my_node变为了/hello/my_node。我们在启动roslaunch发现我们的键盘已经控制不了小乌龟了,因为我们在给节点添加namespace的同时,节点的所有属性包括topic也都被置于namespace之下:
turtlesim_node订阅的是/hello/turtle1/cmd_vel话题,而turtle_teleop_key发布的则是/turtle1/cmd_vel话题,topic都不同server和client不可能正常通信,因此小乌龟不再受键盘控制。
6. 指明输出位置的子级标签
<launch>
<node pkg="turtlesim" type="turtlesim_node" name="my_node"/>
<node pkg="turtlesim" type="turtle_teleop_key" name="my_key" output="screen"/>
</launch>
turtle_teleop_key发布的消息可以输出值控制台,output=”screen”|”log”,消息的输出有两种方式:控制台输出/日志输出。
7. 指定节点工作的当前工作目录的子级标签
<launch>
<node pkg="turtlesim" type="turtlesim_node" name="my_node" cwd="ROS_HOME"/>
<node pkg="turtlesim" type="turtle_teleop_key" name="my_key" output="screen"/>
</launch>
Cwd全称为current working directory,cwd=”ROS_HOME”|”node”。如果cwd=”node”,则该节点的工作目录将被设置为与该节点的可执行文件相同的目录;否则cwd=”ROS_HOME”,则该节点的工作目录详见:ROS/EnvironmentVariables - ROS Wikihttp://wiki.ros.org/ROS/EnvironmentVariables
8. 向节点传递参数的子级标签
<node name="add_two_ints_client" pkg="beginner_tutorials" type="add_two_ints_client" args="$(arg a) $(arg b)" />
向add_two_ints_client节点传递两个参数a和b,其实就如同我们运行如下命令:
rosrun beginner_tutorials add_two_ints_client a_value b_value
② 参数设置标签
<launch>
<param name="var" type="int" value="10"/>
</launch>
我们设置的是global全局参数,我们也可以结合<node>标签设置带有命名空间的私有参数:
<launch>
<node pkg="turtlesim" type="turtlesim_node" name="my_node"/>
<node pkg="turtlesim" type="turtle_teleop_key" name="my_key" output="screen">
<param name="var1" type="int" value="10"/>
</node>
<param name="var" type="int" value="10"/>
</launch>
这样的话我们设置了/my_key/var1私有命名空间下的参数,我们使用rosparam list可以看到如下部分参数:
③ 参数打包输入输出删除的标签
从.yaml文件中读取参数:
<launch>
<rosparam command="load" file="$(find test01)/launch/params.yaml"/>
<node pkg="turtlesim" type="turtlesim_node" name="my_node"/>
<node pkg="turtlesim" type="turtle_teleop_key" name="my_key" output="screen"/>
<param name="var" type="int" value="10"/>
</launch>
使用rosparam list读取出参数如下所示:
除此之外,当我们将.yaml参数文件中的参数导入参数服务器时,我们还可以给这些参数添加namespace命名空间:
<launch>
<node pkg="turtlesim" type="turtlesim_node" name="my_node"/>
<node pkg="turtlesim" type="turtle_teleop_key" name="my_key" output="screen"/>
<param name="var" type="int" value="10"/>
<rosparam command="load" file="$(find test01)/launch/params.yaml" ns="hello"/>
</launch>
结果如下:
变量全都加上的前缀,即命名空间/hello/…。
将参数打包输入进.yaml文件中:
<launch>
<rosparam command="dump" file="$(find test01)/launch/input.yaml"/>
<node pkg="turtlesim" type="turtlesim_node" name="my_node"/>
<node pkg="turtlesim" type="turtle_teleop_key" name="my_key" output="screen"/>
<param name="var" type="int" value="10"/>
</launch>
这样做理应可以将该功能包的所有参数导入到了我们指定的.yaml文件中,但是我们得到的结果确是:
Input.yaml文件中啥变量也没有,我们导入了个寂寞,这是因为rosparam是launch文件命令中优先级最高的,不管你将rosparam命令放在哪里,launch文件总是先执行参数的导入/导出/删除操作。我们要想导入参数必须另建一个.launch文件,在使用上述launch文件启动完所有节点之后,在另一个launch文件中执行该功能包参数的导出操作:
<launch>
<rosparam command="dump" file="$(find test01)/launch/input.yaml"/>
</launch>
此时,input.yaml文件中数据为:
Rosparam不仅可以从.yaml导出导入参数,也可以直接删除参数服务器中的参数:
<launch>
<rosparam command="dump" file="$(find test01)/launch/input.yaml"/>
<rosparam command="delete" param="/hello/n1"/>
</launch>
使用上述代码:删除了“/hello/n1”这个参数:
<launch>
<rosparam command="dump" file="$(find test01)/launch/input.yaml"/>
<rosparam command="delete" ns="hello"/>
</launch>
使用上述代码:删除了“hello”命名空间下的所有参数:
如果啥子级标签也没有,那就删除这个包下的所有参数:
<launch>
<rosparam command="dump" file="$(find test01)/launch/input.yaml"/>
<rosparam command="delete"/>
</launch>
结果如下:
Rosparam可以直接向参数服务器传递数组类型的参数:
<launch>
<node pkg="turtlesim" type="turtlesim_node" name="my_node"/>
<node pkg="turtlesim" type="turtle_teleop_key" name="my_key" output="screen"/>
<param name="var" type="int" value="10"/>
<rosparam command="load" file="$(find test01)/launch/params.yaml" ns="hello"/>
<rosparam param="a_list">[1, 2, 3, 4]</rosparam>
</launch>
我们调用rosparam get a_list命令行命令发现:
A_list参数被参数服务器解析为int类型的数组。也可以用另一种方式向参数服务器传递多个且类型的不同的参数:
<launch>
<node pkg="turtlesim" type="turtlesim_node" name="my_node"/>
<node pkg="turtlesim" type="turtle_teleop_key" name="my_key" output="screen"/>
<param name="var" type="int" value="10"/>
<rosparam command="load" file="$(find test01)/launch/params.yaml" ns="hello"/>
<rosparam param="a_list">[1, 2, 3, 4]</rosparam>
<rosparam>
a: 9
b: "hello"
c: [1,2,3,4]
</rosparam>
</launch>
还可以使用此方式向参数服务器传递带有命名空间的多个不同类型的参数:
<launch>
<node pkg="turtlesim" type="turtlesim_node" name="my_node"/>
<node pkg="turtlesim" type="turtle_teleop_key" name="my_key" output="screen"/>
<param name="var" type="int" value="10"/>
<rosparam command="load" file="$(find test01)/launch/params.yaml" ns="hello"/>
<rosparam param="a_list">[1, 2, 3, 4]</rosparam>
<rosparam>
a: 9
b: "hello"
c: [1,2,3,4]
</rosparam>
</launch>
此外,value标签的写法有两种形式:
<rosparam param="a_list">[1, 2, 3, 4]</rosparam>
<rosparam param="a_list" value="[1, 2, 3, 4]"/>
这两种形式都是等价的,都可以得到:
其实,这就是param和rosparam标签的最主要的区别:
Param标签:向参数服务器传递/修改(覆盖原来的参数就是修改)一个参数,且没有删除操作;
Rosparam标签:向参数服务器传递/修改/删除多个参数、从.yaml文件中加载参数或者将功能包的参数导入.yaml文件当中。
④ 参数统一管理的标签
<launch>
<arg name="car_width" default="[1,2,3,4]" doc="the width of car"/>
<rosparam param="a_list">$(arg car_width)</rosparam>
<rosparam>
Name:
a: 9
b: "hello"
c: $(arg car_width)
</rosparam>
</launch>
Arg标签一般常和param一起使用(不可以和 rosparam标签一起使用),其中$(arg arg_name)代表着<arg name=”arg_name” default=”default_value” doc=”注释说明”>中的default_value。这样我们改变default_value值,launch中与之相关的值都会改变。Arg标签的子标签格式如下:
其中default和value标签只能存在一个,不可以共存!
除此之外,我们使用default而不使用value标签的原因就是“我们使用default时,我们不通过命令行传参那么$(arg arg_name)就是default_value;当我们通过命令行传参时,我们传递的参数会覆盖$(arg arg_name)中默认参数值default_value”,这样我们传参就变得非常灵活了。如下所示,我们使用命令行传递参数:
命令行传参格式为:arg_name:=set_value。Launch文件如下:
<launch>
<arg name="car_width" default="[1,2,3,4]" doc="the width of car"/>
<param name="var" type="int" value="$(arg car_width)"/>
</launch>
我们在命令行调用rosparam get var命令:
但是当你将arg标签和rosparam标签组合在一起使用,则不会得到你想要的结果:
<launch>
<arg name="car_width" default="[1,2,3,4]" doc="the width of car"/>
<rosparam param="a_list" value="$(arg car_width)"/>
</launch>
我们在命令行调用rosparam get a_list命令:
我们想要的是一个数字”2”,但是这个变量却被赋值了字符串。
⑤ 改topic名称的标签
<launch>
<!-- the topic of turtlesim_node is /turtle1/cmd_vel -->
<node pkg="turtlesim" type="turtlesim_node" name="my_node"/>
<remap from="/turtle1/cmd_vel" to="new_topic"/>
</launch>
Remap标签的格式为:
Remap的实质:
我们通过restopic info topic_name可以得知由topic:/turtle1/cmd_vel映射而来的topic:/new_topic其实本质上是相同的,只是名称不同。Remap标签的作用在于:
Remap的前提是topic1和topic2的数据类型相同,仅仅是名称不同而已。这样的话两个话题重映射到new_topic话题上就可以使得通信双方建立联系。切记:topic重映射是产生一个与其数据类型相同仅仅是名称不同的新话题,不是修改原来的话题!
⑥ 节点组织标签
<launch deprecated="this vision is out-of-date!">
<group ns="family">
<!-- the topic of turtlesim_node is /turtle1/cmd_vel -->
<node pkg="turtlesim" type="turtlesim_node" name="my_node"/>
<!-- the topic of turtle_teleop_key is /turtle1/cmd_vel -->
<node pkg="turtlesim" type="turtle_teleop_key" name="my_key" output="screen"/>
<rosparam command="load" file="$(find test01)/launch/params.yaml" ns="hello"/>
<arg name="car_width" default="[1,2,3,4]" doc="the width of car"/>
<rosparam param="a_list" value="$(arg car_width)"/>
<rosparam>
Name:
a: 9
b: "hello"
c: [1,2,3,4]
</rosparam>
<param name="var" type="int" value="$(arg car_width)"/>
</group>
</launch>
结果就是给被<group>…</group>包含的所有参数、节点的属性加上了namespace。对于参数来说:
对于节点的属性(例如:节点订阅/发布的话题)来说:
<group>标签还有一个参数就是:
用于确定是否在启动前删除指定命名空间下的所有参数,和前面的<rosparam command=”delete”/>作用类似。
⑦ 启动其他launch文件的标签
<launch>
<include file="$(find test01)/launch/test01_launch.launch">
<arg name="car_width" default="10"/>
</include>
</launch>
Include标签下有两个子级标签arg和加载环境变量的env,我们一般用arg来传递参数,被该launch加载的launch文件如下所示:
<launch deprecated="this vision is out-of-date!">
<!-- the topic of turtlesim_node is /turtle1/cmd_vel -->
<node pkg="turtlesim" type="turtlesim_node" name="my_node"/>
<!-- the topic of turtle_teleop_key is /turtle1/cmd_vel -->
<node pkg="turtlesim" type="turtle_teleop_key" name="my_key" output="screen"/>
<rosparam command="load" file="$(find test01)/launch/params.yaml" ns="hello"/>
<arg name="car_width" default="[1,2,3,4]" doc="the width of car"/>
<rosparam param="a_list" value="$(arg car_width)"/>
<rosparam>
Name:
a: 9
b: "hello"
c: [1,2,3,4]
</rosparam>
<param name="var" type="int" value="$(arg car_width)"/>
</launch>
被加载的launch文件需要在命令行输入参数才可以运行,我们在<include>…</include>标签下添加<arg…/>子级标签,其实就相当于在命令行传参了。我们编代码是讲究的是复用而非复写,因此include标签很好的帮助我们提高了代码复用的效率使得代码显得更加简洁,也相当于声明了launch文件间的依赖关系,即launch启动的先后顺序。
Include标签还有一个子标签,非常有用。他就是pass_all_args:这个标签表示我是否需要将该launch文件中所有使用arg设置的参数全部加载到使用include标签包含的launch当中:
<launch>
<arg name="car_width" default="10"/>
<include file="$(find test01)/launch/test01_launch.launch" pass_all_args="true"/>
</launch>
我这样做的效果和上一个代码一样,但是我如果include很多launch,只要pass_all_args="true",那么该launch中的所有参数都会自动作为“被include的launch”的参数,这样我们再也不用在include标签下添加那么多arg子级标签用于给每个include的launch添加参数了,这看起来是多么美妙呀!
Note:我们可以使用“include标签+子级标签“来将所有launch文件按照逻辑启动顺序封装在一个launch文件当中,这样我们只需启动一个launch即可以启动整个功能包的全部节点。
功能包/源文件/launch文件组织工具
功能包的组织工具 | 元功能包 |
节点文件(.cpp源文件)的组织工具 | Launch文件 |
Launch文件的组织工具 | Launch文件 |
一层一层的管理,文件组织形式如下所示:
一层一层的管理,文件组织形式如下所示:
Launch中的if-else
其实在每个标签中都有用于if逻辑判断的子级标签:
<launch>
<arg name="trueorfalse" default="[1,2,3,4]" doc="true or false"/>
<rosparam if="$(arg trueorfalse)" param="a_list">[1, 2, 3, 4]</rosparam>
</launch>
if=”true|false”,if后面的参数只能是bool类型,并且if=”true”就会执行该句语句。与之相反的是unless,译为“除非…”,格式与if相同:unless=”true|false”,如果unless=”true”则相当于if=”false”的作用:
<launch>
<arg name="trueorfalse" default="[1,2,3,4]" doc="true or false"/>
<rosparam unless="$(arg trueorfalse)" param="a_list">[1, 2, 3, 4]</rosparam>
</launch>
如果我们用于逻辑块的if判断,我们可以将if/unless和<group>…</group>标签相结合:
<launch deprecated="this vision is out-of-date!">
<arg name="trueorfalse" default="[1,2,3,4]" doc="true or false"/>
<group if="$(arg trueorfalse)">
<!-- the topic of turtlesim_node is /turtle1/cmd_vel -->
<node pkg="turtlesim" type="turtlesim_node" name="$(anon my_node)"/>
<!-- the topic of turtle_teleop_key is /turtle1/cmd_vel -->
<node pkg="turtlesim" type="turtle_teleop_key" name="my_key" output="screen"/>
<rosparam command="load" file="$(find test01)/launch/params.yaml" ns="hello"/>
<rosparam param="a_list">[1, 2, 3, 4]</rosparam>
<rosparam>
Name:
a: 9
b: "hello"
c: [1,2,3,4]
</rosparam>
</group>
</launch>
这样就将多条逻辑语句同时包含在if之下,相当于C++中if{…}语句中的“以大括号为边界的逻辑块”。