第三章:参数服务器、命名空间与launch文件大全解
本章为了介绍清楚参数服务器、命名空间。顺带将luanch文件中相关参数标签的用法都做了详细解释。
一、参数服务器
1. 概念
参数类似ROS中的全局变量。由ROS Master进行管理。
值得注意的是: talker向master更改了参数数值,如果listener不去重新查询的话,仍然保持原来数值。
想要实现动态的参数配置的话,需要用到ROS的功能包:dynamic_reconfigure
,教程之后会在ROS进阶系列对该功能包做详细说明。
2. 使用终端命令维护参数
rosparam终端命令简单明了,如下所示:
3. launch文件维护参数
<param>
标签
<param name="XXX" value="XXX" >
<rosparam>
标签
当在复杂系统中,每次启动需要很多的参数配置,挨个像上述param
标签的设置方法太麻烦了。ros提供一种批量解决的方案:
<rosparam file="***.yaml" command="load" ns="XXX">
其中需要指定三个参数,第一是yaml文件位置,第二是使用load犯法,第三可以指定命名空间。
这种方法与在终端使用:rosparam load ***.yaml
效果相同。
4. node 节点中维护参数
当然,我们可以编写程序,在node节点中对参数服务器进行维护。
4.1. roscpp的参数API
设置参数
ros::NodeHandle nh;
nh.setParam("/global_param", 5);
nh.setParam("relative_param", "my_string");
nh.setParam("bool_param", false);
读取参数
ros::NodeHandle nh;
std::string global_name, relative_name, default_param;
// 使用nh句柄来获取参数
if (nh.getParam("/global_name", global_name))
{
...
}
// 如果没有读取到,就按照default_value来设置参数
// 所以要注意后两个参数的数据格式保存一致
nh.param<std::string>("default_param", default_param, default_value);
查询参数是否存在
ros::NodeHandle nh;
if (nh.hasParam("my_param"))
{
...
}
删除参数
ros::NodeHandle nh;
nh.deleteParam("my_param");
4.2. rospy的参数API
设置参数
# Using rospy and raw python objects
rospy.set_param('a_string', 'by_rospy')
rospy.set_param('~private_int', 2)
rospy.set_param('list_of_floats', [1., 2., 3., 4.])
rospy.set_param('bool_True', True)
rospy.set_param('gains', {'p': 1, 'i': 2, 'd': 3})
读取参数
global_name = rospy.get_param("a_string")
private_param = rospy.get_param('~private_int')
default_param = rospy.get_param('list_of_floats', '[1., 2., 3.]')
# fetch a group (dictionary) of parameters
gains = rospy.get_param('gains')
p, i, d = gains['p'], gains['i'], gains['d']
查询参数是否存在
if (rospy.has_param('to_search')):
rospy.delete_param('to_search')
删除参数
try:
rospy.delete_param('to_delete')
except KeyError:
print("value not set")
二、命名空间
1. 概念
ROS中,节点、参数、话题、服务等命名的时候,都会涉及到命名空间的相关概念。
namespace命名是ROS封装的一种重要机制。命名空间之间的资源可以通过正确的命令来相互访问。
全局(global)名称
举例:/gloabl/name
首字符是/
的名称是全局名称。可以在全局范围内直接访问
相对(relative)名称
举例: relative/name
相对名称是ros提供默认命名空间。不需要开头的左斜杠。如例子中,如果我们设置默认命名空间为relative,那么在程序中只需要写name即可。如果其他程序想要访问的话,使用/relative/name全局名称来搜索。
ros提供三种默认命名空间的设置:
- 通过命名参数:
ros::NodeHandle nh("namespace");
- launch文件中设置ns参数
- 使用环境变量(很少见)
私有(private)名称
节点内部私有资源,只在节点内部使用。以~
开始
私有名称不使用当前默认命名空间,而是用节点的全局名称作为命名空间。
举例:~cmd_vel
, 当前节点名称为/hello/hello_node
那么该私有名称解析为全局名称为:/hello/hello_node/cmd_vel
举例
我们使用第一章中撰写的cpp_talker节点,然后使用设置默认命名空间第一种方法来进行测试:
用例1:
ros::NodeHandle nh;
ros::Publisher pub = nh.advertise<learn_topic::person>("person_topic", 1);
输出topic名称:/person_topic
解释:没有设置默认命名空间,故全局的解析就是其本身。
用例2:
ros::NodeHandle nh("namespace");
ros::Publisher pub = nh.advertise<learn_topic::person>("person_topic", 1);
输出topic名称:/namespace/topic_person
解释:设置了默认空间,全局解析为:默认空间 + 自定义topic名称
用例3:
ros::NodeHandle nh("namespace");
ros::Publisher pub = nh.advertise<learn_topic::person>("/person_topic", 1);
输出topic名称:/person_topic
解释:设置了默认空间,但是topic首字符为/
,说明topic本身就是全局名称,默认空间无效
用例4:
ros::init(argc, argv, "cpp_talker");
ros::NodeHandle nh("~");
ros::Publisher pub = nh.advertise<learn_topic::person>("person_topic", 1);
输出topic名称:/cpp_talker/person_topic
解释:私有名称不使用当前默认命名空间,而是用节点的全局名称作为命名空间,节点的全局名称为/cpp_talker
2. 重映射
两种方法实现重映射:
- 在启动节点时:
举例:
rosrun learn_talker talker chatter:=/talker/chatter
将原本的topic:chatter
更换为/talker/chatter
- 在launch文件中:
举例:
<launch>
<node name="talker" pkg="learn_talker" type="cpptalker" >
<remap from="chatter" to="/talker/chatter"/>
</node>
</launch>
上述例子中关键在于<remap>
标签,from 原来的发布者话题,to现有的订阅者话题名字
所以出现在talker的node中,含义就是,将自己现在发布的某个话题转变为另外的一个名字;而出现在listener的node中,含义就是,将发布者已经发布的某个话题转变为自己订阅的话题名字。
顺便将node中其他常用参数的含义在这里说明下。
- name: 该节点的名字,写在launch的节点名字将在启动后把原本的
init()
函数中的node_name覆盖 - pkg:节点属于的功能包名字
- type:节点对应的可执行文件名称(cpp中就是Cmakelists中的命名,python文件就是本身文件名字)
- output=“screen” :将节点的标准输出打印在终端
- ns = “your_namespace”: 自己起的默认命名空间
- args=“XXX”: 节点需要输入的参数
3. 命名空间对参数的影响
本部分,我们特地设计了一套测试用例,来验证我们上述所学到的内容。
首先:
launch文件如下:
<launch>
<param name="a_string" value="global_value_by_launch" />
<node pkg="learn_param" type="learn_param_cpp_node" name="node_name" ns="namespace" output="screen">
<param name="a_string" value="local_value_by_launch" />
</node>
</launch>
launch文件中需要注意的是:
- 使用
<param>
标签在node标签外设置全局参数 - 使用
<param>
标签在node标签里设置局部参数 - 使用命名空间:
namespace
- 使用node节点名称为:
node_name
测试用例
#include <ros/ros.h>
#include <ros/param.h>
int main(int argc, char **argv) {
std::string global_name, local_name("default_local_name"), private_name("default_private_name");
ros::init(argc, argv, "node_name");
ros::NodeHandle nh;
ros::NodeHandle private_nh("~");
nh.setParam("local_nh_set", "local_name");
if(nh.hasParam("/namespace/local_nh_set")){
std::cout << "local_nh_set is ok! " <<std::endl;
}
nh.setParam("/global_nh_set", "global_name");
private_nh.setParam("local_pri_nh_set", "pri_local_name");
if(nh.hasParam("/namespace/node_name/local_pri_nh_set")){
std::cout << "local_pri_nh_set is ok! " <<std::endl;
}
if (nh.hasParam("local_pri_nh_set")){
std::cout << "local_pri_nh_set: also can get by nh" <<std::endl;
}else{
std::cout << "local_pri_nh_set: can not get by nh" <<std::endl;
}
private_nh.setParam("/global_pri_nh_set", "pri_global_name");
nh.getParam("/a_string", global_name);
std::string local_name_key(nh.getNamespace() + "/node_name" + "/a_string");
nh.param("local_name_key", local_name, local_name);
private_nh.param("a_string", private_name, private_name);
std::cout << "global_name: " << global_name << std::endl;
std::cout << "local_name: " << local_name << std::endl;
std::cout << "private_name: " << private_name << std::endl;
ros::spin();
return 0;
}
在上述用例中,我们需要注意的是:
- 分别设置了全局
nh
和局部private_nh
- 使用全局
nh
和局部private_nh
分别建立了全局param和局部param - 分别使用全局
nh
和局部private_nh
来获取launch文件中的参数具体数值
实验结果
编译运行之后,我们查看实验结果:
查看所有list的参数:
实验结论
-
所有的参数名称都会被分为三种,全局、局部、私有参数;最终都会解析为全局参数的形式,如上图list所示。
-
写在launch文件node标签里面的param标签,相当于私有参数
-
全局句柄访问私有参数的时候,需要写全名;私有句柄直接写名称即可。
未来写代码的建议
- node文件都使用launch文件来管理,哪怕是一个node节点。这样可以统一管理node名称和namespace
- 在node里面初始化nh句柄的时候,统一使用
ros::NodeHandle nh(~);
的形式。 - 所有根本node相关的外部参数,如果使用launch文件设置的话,一律都写到
<node>标签
管理范围内