1.6 ROS的其他重要概念
除了上面介绍的话题、服务和动作的核心概念,在做实际项目时还有一些重要的概念也会经常用到,这里进行一个简短的梳理,帮大家先建立起整体的概念。这样在具体使用过程中需要了解相关概念的细节时,就可以上网快速查找对应概念的知识点了。
1. parameter
静态参数的使用就比较简单了,在节点程序中使用getParam方法可以获取参数值,用setParam方法设置参数值,如下面的用法。也可以通过在launch文件启动节点时向节点传参的方式来设置参数值。
// 获取参数的值 nh.getParam("com_port",com_port); // 设置参数的值 nh.setParam("com_port","/dev/ttyUSB0");
动态参数的用法相对复杂一点,需要先在功能包的cfg目录下新建*.cfg文件,在*.cfg文件里面添加需要的动态参数,*.cfg文件采用Python脚本进行编写。然后还需要在功能包的CMakeList.txt里面进行动态参数相关的编译配置,这与前面讲过的自定义服务消息类型和动作类型有点类似。最后就可以在节点中引用动态参数头文件,然后启动动态参数的服务端,监听客户端发送的参数修改请求,并实时地维护动态参数的值。我们可以在其他节点中使用动态参数客户端来发起参数修改请求,也可以使用rqt_reconfigure工具直接修改参数值。其实,动态参数机制与上面讲过的服务通信机制非常相似。具体实现代码就不展开了,有兴趣的读者可以参考官方wiki教程[1]。
2. tf
一个机器人系统中通常会有多个三维参考坐标系,机器人中的坐标系使用的是右手坐标系,而且这些坐标系之间的相对关系会随时间推移而变化。这里以一个实际机器人应用场景的例子来说明这种关系和变化。该例子中激光SLAM构建出来的栅格地图的坐标系为map,机器人底盘的坐标系为base_footprint,激光雷达、IMU传感器的坐标系分别为base_laser_link、imu_link,这些坐标系之间的关系有些是静态的、有些是动态的。比如在机器人底盘移动的过程中,机器人底盘与世界的相对关系map->base_footprint就会随之变化;而安装在机器人底盘上的激光雷达、IMU这些传感器与机器人底盘的相对关系base_footprint->base_laser_link、base_footprint->imu_link就不会随之变化。
坐标及坐标转换在机器人系统中非常重要,特别是机器人在环境地图中自主定位和导航、机械手臂对物体进行复杂的抓取任务时,都需要精确地知道机器人各部件之间的相对位置及机器人在工作环境中的相对位置。因此ROS专门提供了tf这个工具用于简化这些工作。tf可以让用户随时跟踪多个坐标系的关系,机器人各个坐标系之间的关系是通过一种树形数据结构来存储和维护的,即tf tree。借助这个tf tree,用户可以在两个坐标系中任意时间将点、向量等数据的坐标值完成变换。
在节点中使用tf分为两个部分:广播tf变换和监听tf变换。具体编程实例就不展开了,有兴趣的读者可以参考官方wiki教程[2]。
3. urdf
机器人中的机械模型用urdf来描述,机器人机械模型在导航避障、机械臂抓取、建图等应用中非常重要。一般是专门新建一个功能包,在里面编写urdf文件,并利用模型发布工具将urdf文件描述的内容进行发布。urdf文件中可以描述各种几何物体的形状,还可以描述部件之间的tf关系。因此,除了可以在launch文件中设置静态tf关系外,还可以直接在urdf文件中设置静态tf关系,但是要注意两种设置方法不要重复。关于urdf的使用与编程,可以参考官方wiki教程[3]。
4. launch
在一个大型的机器人项目中,经常涉及多个node协同工作,并且每个node都有很多可设置的参数。比如第13章讨论的xiihoo机器人导航项目,涉及地图服务节点、定位算法节点、运动控制节点、底盘控制节点、激光雷达数据获取节点等,和几百个影响着这些节点行为模式的参数。如果全部手动逐个启动节点并传入参数,工程的复杂程度将难以想象。这个时候就需要用roslaunch来解决问题,将需要启动的节点和需要设置的参数全部写入一个*.launch文件,然后用roslaunch一次性地启动*.launch文件,这样所有的节点就轻而易举地启动了。launch文件采用XML文本标记语言进行编写,xiihoo机器人导航项目的launch文件如代码清单1-7所示。
代码清单1-7 xiihoo机器人导航项目的launch文件xiihoo_nav.launch
<launch> <!-- Map server --> <arg name="map_path" default="/home/ubuntu/map/carto_map.yaml"> <node name="map_server" pkg="map_server" type="map_server" args="$(arg map_path)"/> <!-- Run AMCL --> <arg name="initial_pose_x" default="0.0"> <arg name="initial_pose_y" default="0.0"> <arg name="initial_pose_a" default="0.0"> <include file="$(find amcl)/launch/amcl.launch"> <arg name="initial_pose_x" value="$(arg initial_pose_x)"> <arg name="initial_pose_y" value="$(arg initial_pose_y)"> <arg name="initial_pose_a" value="$(arg initial_pose_a)"> </include> <!-- Run move_base --> <node pkg="move_base" type="move_base" respawn="false" name="move_base" output="screen" clear_params="true"> <rosparam file="$(find xiihoo_nav)/config/move_base_params.yaml" command="load" /> <rosparam file="$(find xiihoo_nav)/config/costmap_common_params.yaml" command="load" ns="global_costmap"/> <rosparam file="$(find xiihoo_nav)/config/costmap_common_params.yaml" command="load" ns="local_costmap" /> <rosparam file="$(find xiihoo_nav)/config/global_costmap_params.yaml" command="load" /> <rosparam file="$(find xiihoo_nav)/config/local_costmap_params.yaml" command="load" /> <rosparam file="$(find xiihoo_nav)/config/navfn_planner_params.yaml" command="load" /> <rosparam file="$(find xiihoo_nav)/config/base_local_planner_params.yaml" command="load" /> </node> </launch>
可以发现launch能够嵌套使用,也就是在一个launch中可以嵌套调用另一个launch。另外,launch文件中设置参数的形式也有很多种,比如可以通过param标签为某个参数指定固定值,也可以通过arg标签从外部载入参数的设置值,还可以通过rosparam标签从外部文件批量载入参数的设置值。
launch文件的使用很简单,首先在相应功能包目录下新建一个launch文件夹,然后在launch文件夹中新建*.launch文件,并按照launch标签规则编写好launch文件的内容。关于launch标签规则,有兴趣的读者可以参考官方wiki教程[4]。最后在终端中用roslaunch命令启动launch文件,启动xiihoo_nav.launch的命令如下。
roslaunch xiihoo_nav xiihoo_nav.launch
5. plugin
ROS支持功能包的动态加载和卸载,这个功能由一个C++的pluginlib库来实现。以导航功能包move_base来说,我们可以在move_base运行的过程中,动态加载Class对象、动态函数库等,像代价地图、路径规划这些功能包都是通过插件机制动态加载到move_base中的。使用插件来扩展和升级程序的功能很方便,不用改动原有程序的源码也不用重编译,就能动态加载和卸载功能。关于插件的用法,有兴趣的读者可以参考官方wiki教程[5]。
6. nodelet
通常情况下,我们使用的ROS节点都是独立的可执行文件,每个节点启动后在系统里都是以一个独立的进程存在的,即节点之间的通信就是进程间的通信,并且通信过程需要消耗网络的带宽。为了提高通信的效率和减少网络带宽的占用,ROS中有一类特殊的节点——nodelet,这类节点可以在单个进程下以多个线程的形式运行,这样节点间的通信就是线程间的通信了。比如摄像头这类数据量大的传感器,可以使用nodelet方式与其他节点通信,从而大大提高传输效率。关于nodelet的用法,有兴趣的读者可以参考官方wiki教程[6]。
[1]参见http://wiki.ros.org/dynamic_reconfigure/Tutorials。
[2]参见http://wiki.ros.org/tf/Tutorials。
[3]参见http://wiki.ros.org/urdf/Tutorials。
[4]参见http://wiki.ros.org/roslaunch/XML。