之前我们一直在用rosrun跑程序,格式如下  
rosrun package_name executable_file_name
 
在使用rosrun之前,我们一定得需要启动rosmaster,即开启一个窗口输入roscore.使用rosrun就只能单纯地跑一个节点,当我们要写大型的程序时,有时候会涉及到跑同时很多个节点,这时候就需要使用roslaunch了.绝大部分信息也就是来源于官网了.见http://wiki.ros.org/roslaunch/XML/node 下面我们谈谈roslaunch的第一个功能.

利用roslaunch同时启动数个节点

我们先启动一个节点,直接利用以前的程序吧.我们尝试利用roslaunch开启pub_int8这个节点. 1:在我们最开始建立的pub_sub_test这个文件夹下建立一个新的文件夹,名字叫launch 2:在launch中建立的一个文件,名字可以随意,后缀必须是launch.我们起名为pub_int8.launch 3:用gedit或者你自己的IDE打开launch文件,输入下面的内容
<launch>
    <node name="pub_int8" pkg = "pub_sub_test" type = "pub_int8">
    </node>
</launch>
   
roslaunch使用的是xml语言,不过这种语言不需要专门学习了,大家就看这个例子就懂了.launch文件的内容是跑一个node最简单的形式.<launch>,<\launch>表示launch文件的开始和结束.要开始一个节点,那么内容很简单,第二行,<node ....>表示接下来输入node相关的内容,比如说首先输入的是node的名字,这个东西一般和type后面输入的内容一样,type需要被赋值为节点对应的可执行文件的名字,name则是节点的名字.具体区别是你在CMakeLists.txt文件里编译文件的命令
   
add_executable(abc ABC.cpp)
    中abc就是你需要填写在type后的内容,而填写在name后的内容,就是你的节点名字,自行选择,你ros程序中
ros::init(argc, argv, "abcde");
   
的这一行的abcde也是节点名。如果你程序中起的节点名字和launch文件中name后面对应的名字不同,ros会采用name后面的名字作为节点名。当你使用rqt_graph观察时,你看到的代表此程序的名字就是abcde了。一般我们把name和type后都接一样的名字,比如这儿都用的pub_int8。但是如果我们有时候需要把一个可执行文件同时作为数个节点运行,我们就需要给他们不同的节点名,即相同的type不同的name。这个可能现在听起来有点迷糊,在第九讲中会有一个例子讲到。 即我们在cmakelists中add_executable(A a.cpp b.cpp)的A. 这里就是pub_int8,pkg参数被赋值为节点存在于哪个package,我们这儿自然是pub_sub_test</node>表示要输入node相关的信息结束. 其实总的来说,roslauch的最简形式,和我们使用rosrun差不多,指定了那个package和哪个node.接下来就是跑程序了. 4:打开一个terminal,进入到我们创建的workspace,即之前创建的catkin_ws文件夹,和跑rosrun之前一样,我们先source.
cd catkin_ws
source devel/setup.bash
   
和跑rosrun之前不一样的是,如果我们没有开一个terminal跑roscore,运行roslaunch文件后rosmaster会自动启动.当然你关闭了roslaucn之后rosmaster也会关闭. 5:运行roslaunch.接着在你的terminal中输入
   
roslaunch pub_sub_test pub_int8.launch
   
格式和使用rosrun时相似,roslaunch package_name launch_file_name 这时候节点就会跑起来开始发布消息了.我们可以用rosrun跑sub_int8的程序来检查一下. 打开另外一个terminal
cd catkin_ws
source devel/setup.bash
rosrun pub_sub_test sub_int8
     
这时候可以看到在sub_int8节点中接收到信息. 如果你对之前我们刚刚写pub_int8.cpp时有印象,那么应该记得我们跑了pub_int8节点后,应该能看到发布的消息的内容,但是我们在刚才用roslaunch跑了节点后确没有看到.这是为什么呢?原来使用roslaunch时,它默认把来自节点print出来的信息存放到一个log文件里面二不直接print在屏幕上.那么我们如何显示出来呢?根据http://wiki.ros.org/roslaunch/XML/node 中Attribute的选项,我们可以看到除了我们使用的pkg, type, name外还有很多optional的选项.其中一个是output, output下面写的内容表示的就是rosluanch默认不print消息,如果要print,我们需要给output赋值为screen.模仿name, pkg这几个东西的赋值方式,我们只需要把roslaunch文件修改成下面的样子就可以了
   
<launch>
    <node name="pub_int8" pkg = "pub_sub_test" type = "pub_int8" output = "screen">
    </node>
</launch>
   
这时候再跑这个launch文件,我们就可以看到terminal中显示了我们发布的消息了.可以看到Attribute里还有很多东西呢,以后都有可能用到.比如说你跑程序时在可执行文件后面经常有args,通过看attribute来知道怎么在launch文件中输入好你的args. 接下来我们同时启动两个节点.我们同时启动以前写过的pub_int8和pub_string. 其实非常简单, 再相同的位置再写一个叫double_pub.launch的文件,在里面输入下面的内容.
<launch>
    <node name="pub_string" pkg = "pub_sub_test" type = "pub_string" output = "screen">
    </node>
    <node name="pub_int8" pkg = "pub_sub_test" type = "pub_int8" output = "screen">
    </node>
</launch>
     
我们就是把定义pub_int8的launch文件中多加了行类似的pub_string的东西,大家进行简单的比对就能看懂.以同样的方式跑这个launch文件,我们就可以看到两个publisher在同时发布消息.

利用roslaunch传递参数

这个部分是相当有用了.某些固定的参数我们在程序运行前就写好的,或者需要经常改变的,我们都不希望写在程序中,因为如果写在了程序中我们每次修改参数都需要重新build一遍程序.比如说我们调整机器人的运动模型的噪音(在卡尔曼滤波中过程噪音和测量噪音是需要人工调整的,不了解也没关系),总之有个参数,我们经常需要改变,把这个参数称为noise.为了简便起见,咱们写一个非常简单的程序,只是把这个传进程序的参数print出来. 考虑到这是一个新的任务了,我们不再把程序写到pub_sub_test这个package里.我们重新建立一个新的名叫read_param_test的pakcage. 还记得怎么建立一个新的pakcage么?
cd catkin_ws/src
catkin_create_pkg read_param_test roscpp rospy std_msgs
cd ..
catkin_make
      这就建立好一个新的pakcage了.在这个pakcage的src文件夹中我们建立一个名叫show_param.cpp的文件.在文件中输入下面的内容    
#include "ros/ros.h"

int main(int argc, char **argv){

    ros::init(argc, argv, "show_param");

    ros::NodeHandle nh;

    double noise;
    nh.getParam("noise", noise);

    ROS_INFO("noise parameter is................... %f", noise);
};
      保存退出后,进入该package的CMakeLists.txt,编译show_param.cpp,在CMakeLists中加入    
add_executable(read_param src/show_param.cpp)
target_link_libraries(read_param ${catkin_LIBRARIES})
      保存退出后使用catkin_make编译. 之后在package中创建一个launch文件夹,在launch文件夹中创建一个名叫read_param.launch的文件,写入如下内容。    
<launch>
    <param name = "noise" type = "double" value = "10.0" />
    <node name="read_param" pkg = "read_param_test" type = "read_param" output = "screen">
    </node>
</launch>
     
我们把show_param.cpp和这个launch文件结合起来读一下。 在show_param.cpp中,到double noise的部分大家应该已经熟悉了,之后的一行是 nh.getParam("noise", noise) nh是我们之前定义的nodehandle了,getParam为获取参数的函数,函数的参数,第一个是"noise"这个noise对应的是你在launch文件里为要传递的参数取的名字,即read_param.launch中param name后面跟的那个"noise";getParam的第二个参数是你在程序中定义的变量的名字,即我们定义的double noise。我们可以看出param中的name和程序中的变量名不需要一样,但是在实际使用中,我们为了不让自己搞混,通常param里给变量什么名字在launch文件里就给变量什么名字。 程序中定义的变量类型和launch文件中type参数所赋的值保持,都是double. launch文件中通过给value一个double类型的数值给变量赋值。 经过上面的操作,launch文件中的10就会传给程序中的noise。 下面我们来跑一下程序,打开terminal,输入
   
cd catkin_ws
source devel/setup.bash
roslaunch read_param_test read_param.launch
     
注意一下,通常我们source 之后,输入roslaunch或者rosrun,按空格之后输入package的前几个字母按tap建可以获取完整的package的名字,但是如果这是一个新建的package我们第一次使用时需要手动把整个名字输进去才行。 程序跑起来后我们就可以看到terminal中有一行
   
[ INFO] [1550108193.348086703]: noise parameter is................... 10.000000
 
证明参数已经读取成功了。下面我们再传递一下srting类型,向量类型的参数。 完全一样的方法,我们如下修改launch file
<launch>
    <param name = "noise" type = "double" value = "10.0" />
    <param name = "string_var" type = "string" value = "abc" />
    <node name="read_param" pkg = "read_param_test" type = "read_param" output = "screen">
    </node>
</launch>
     
可以看到我们只是添加了一行代码,定义变量名字叫string_var,值为abc,类型为string。自然要在程序中读取这个参数你就需要在代码中定义一个string类型的变量,用nh.getParam读取,这儿不再赘述。如果我们想传递一个数组呢?其实是不能直接通过<param...>这种形式直接添加一个数组的。我们可以参考官网 http://wiki.ros.org/roslaunch/XML/param 在Attributes, type那一行我们可以看到type一共有
"str|int|double|bool|yaml"(optional)
 
五种形式,其中并不包含数组。 要从外部传递数组的话,需要使用rosparam,简单来讲,把launch添加一行    
<launch>
    <param name = "noise" type = "double" value = "10.0" />
    <param name = "string_var" type = "string" value = "abc" />
    <rosparam param="a_list" >[1, 2, 3, 4]</rosparam>
    <node name="read_param" pkg = "read_param_test" type = "read_param" output = "screen">
    </node>
</launch>
      在程序中添加两行    
std::vector<int> a_list;
nh.getParam("a_list",a_list)
     
即可把[1,2,3,4]这个vector传递到程序中的a_list中。 当我们遇到参数很多的程序时,我们需要使用yaml文件。什么是yaml文件呢?yaml文件是使用一种特定结构来储存外部参数的文件。先不说我们要传递数组什么的,设想你有一个大型的程序,需要从外部读取数十个上百个参数,你把这些参数全部用<param...>形式写到launch文件中会让launch文件显得特别冗长,使用起来并不友好。而有了yaml文件的帮助,我们可以把所有要读取的参数全部写到yaml文件中去,然后launch文件中只需要一行代码读取yaml   文件即可。yaml文件内容的固定格式是
 
变量名: 变量值
    我们在read_param_test这个pakcage中创建一个叫config的文件夹,在该文件夹里面创建一个叫read_param_test.yaml的文件,打开并输入下面的内容。
noise: 10.0
string_var: abc
vector_var: [1,2,3]
   
前两个变量是我们之前在launch文件中通过写<param...>传递的参数,现在我们试着通过yaml文件传递一下参数。至于第三个一看就是个向量(数组)了。 注意在yaml文件中我们不需要指定变量类型什么的了,变量名: 变量值是它的固定格式,变量值包含非数字的量它会自动认为这是string类型变量,纯数字的值,如果不包含小数则会认为是int类型的变量,包含小数则是double类型的变量。 接着我们写一个新的launch文件来读取这个yaml文件,进而能把yaml文件的值传递到程序中去。我们在之前建立的launch文件夹中新建一个launch文件,叫read_param_from_yaml.launch。写入下面的内容
   
<launch>
    <rosparam file="$(find read_param_test)/config/read_param_test.yaml" command="load" />  
    <node name="read_param" pkg = "read_param_test" type = "read_param" output = "screen">
    </node>
</launch>
     
可以看到launch文件的开头和结尾,<node...>那一行和之前写的读取参数的launch文件相比没有变化,变化的是我们有了一行<rosparam...>来读取yaml文件。 rosparam后面接的第一个参数file用来指定yaml文件的地址,其中$(find read_param_test)表示read_param_test这个package的路径,之后接我们建立的config文件夹,再接刚刚创建的read_param_test.yaml。第二个参数表示需要加载即读取yaml文件中的参数。 接下来就是我们的读取程序了。其实如果不需要读取那个向量的话其实我们程序不需要修改,不过这儿要读取向量我还是把程序写出来吧。
#include "ros/ros.h"
#include <string>
#include <vector>

int main(int argc, char **argv){

    ros::init(argc, argv, "show_param");

    ros::NodeHandle nh;

    double noise;
    if(nh.getParam("noise", noise))
        ROS_INFO("noise is %f", noise);
    else
        ROS_WARN("didn't find parameter noise");

    std::string string_var;
    if (nh.getParam("string_var", string_var))
        ROS_INFO("string_var: %s", string_var.c_str());
    else
        ROS_WARN("No string_var name message");

    std::vector<int> a_list;
    if (nh.getParam("a_list",a_list))
        ROS_INFO("get a_list");
    else
        ROS_WARN("didn't find a_list");
    
    std::vector<int> vector_var;
    if (nh.getParam("vector_var",vector_var))
        ROS_INFO("got vector");
    else
        ROS_WARN("didn't find vector");
};
    我们把寻找参数的代码稍微修改了一下。人们在写yaml文件或者程序中的变量时常常手误,二者名字匹配不上还觉得自己写对了呀。这时候来个类似于    
    if(nh.getParam("noise", noise))
        ROS_INFO("noise is %f", noise);
    else
        ROS_WARN("didn't find parameter noise");
   
这几行代码会让人很愉快。nh.getParam是有返回值的,如果找到了参数,则返回true,没有找到则返回false,基于这一点,我们可以在找不到时利用ROS_WARN输出信息,如果找到了变量,则输入它的值之类的,让我们能更方便地debug。 好了yaml文件,launch文件,程序都弄好了,接下来使用catkin_make编译好程序后。使用
roslaunch read_param_test read_param_from_yaml.launch

    跑程序,你就应该能看到想要的结果了。

发现一个有意思的东西

如果我在一个terminal中开启了roscore,随后跑我们第一个launch文件    
roslaunch read_param_test read_param.launch

  然后再跑第二个launch文件    
roslaunch read_param_test read_param_from_yaml.launch
 
我发现跑第二launch文件时,a_list竟然被赋值了,保留的是运行read_param.launch之后所读取的值。要知道我的yaml文件里根本就没有a_list。同样我要是先跑第二个launch文件,再跑第一个,vector_var被赋值了,保留第一个launch文件读取的那个vector_var的值。 如果我不启动roscore,由于没有启动roscore的情况下launch文件会自动启动roscore,launch文件结束自动关闭,则我跑两个launch文件互不影响。即先跑第一个,再跑第二个,a_list并不会被保留下来。 难道同一个roscore会保留同一个node以前读取的内容?即时程序已经结束?这个我可并不希望,哈哈,我也需要去ROS的官网论坛咨询一下了。

总结

这一讲讲了launch文件跑程序和读参数的基本用法。其实还远远不够。就拿参数来说,你可能在以后的yaml文件中看到
abc:
  d: 1
  e: 2
  f:  3
   
等各种稍微复杂的写法。这种该怎么读呢?另外你如果想通过程序来写yaml文件,该怎么写呢?这些东西就在你需要的时候慢慢琢磨了。个人推荐当你需要更复杂的形式时,不妨看看其他大型的ROS程序,如果他们有读写yaml文件的部分,通过看他们的代码就能学到这些信息。呼呼拖了好久总算把这讲写完了。下一讲我们说一说使用rosbag的一些基本事项。