7.1.6-ROS 运行管理
使用 launch 文件管理 ROS 节点
ROS 中的 launch 文件是 XML 格式的文件,用于高效地启动和管理多个 ROS 节点。本节介绍 launch 文件中可用的各种标签,包括它们的属性和用例。
<launch> 标签
<launch> 标签是每个 launch 文件的根标签,作为所有其他标签的容器。
1. 属性
- deprecated="弃用说明"
向用户提示当前 launch 文件已被弃用。
2. 子标签
- launch 文件中所有其他标签都是
<launch>标签的子元素。
示例:
<launch>
<!-- Other tags go here -->
</launch><node> 标签
<node> 标签用于指定要启动的 ROS 节点。它是 launch 文件中最常用的标签之一。请注意,roslaunch 命令不保证节点按声明顺序启动,因为节点的启动过程是多线程的。
1. 属性
-
pkg="package_name"
指定该节点所属的软件包。 -
type="nodeType"
节点类型,对应可执行文件名。 -
name="nodeName"
节点在 ROS 网络拓扑中的名称。 -
args="xxx xxx xxx" (可选)
向节点传递参数。 -
machine="machine_name"
指定要启动节点的机器。 -
respawn="true | false" (可选)
指定节点退出后是否自动重启。 -
respawn_delay="N" (可选)
若respawn为 true,则定义节点重启前等待 N 秒。 -
required="true | false" (可选)
指定该节点是否为关键节点。若为 true,当该节点退出时整个roslaunch进程将被终止。 -
ns="namespace" (可选)
在指定的命名空间下启动节点。 -
clear_params="true | false" (可选)
在节点启动前清空其私有命名空间下的所有参数。 -
output="log | screen" (可选)
指定日志输出位置:写入日志文件或屏幕。默认是log。
2. 子标签
- env: 用于设置环境变量。
- remap: 用于重映射话题或服务名。
- rosparam: 用于设置参数。
- param: 用于设置参数。
示例:
<launch>
<node name="node1" pkg="my_package" type="node_executable" output="screen" respawn="true" respawn_delay="5">
<param name="param_name" value="param_value"/>
<remap from="/old_topic" to="/new_topic"/>
</node>
</launch><include> 标签
<include> 标签用于将另一个 XML 格式的 launch 文件包含进当前 launch 文件中,从而实现模块化与可复用的配置。
1. 属性
-
file="$(find package_name)/path/to/file.launch"
指定要包含的 launch 文件路径。 -
ns="namespace" (可选)
在指定的命名空间下包含该文件。
2. 子标签
- env: 用于设置环境变量。
- arg: 用于向被包含的 launch 文件传递参数。
示例:
<launch>
<include file="$(find my_package)/launch/another_launch_file.launch" ns="my_namespace"/>
</launch><remap> 标签
<remap> 标签用于重映射 ROS 话题或服务名。在避免名称冲突或在不同节点间统一命名时很有用。
1. 属性
-
from="xxx"
原始的话题或服务名。 -
to="yyy"
话题或服务的新名称。
2. 子标签
- 无
示例:
<launch>
<node name="node1" pkg="my_package" type="node_executable">
<remap from="/old_topic" to="/new_topic"/>
</node>
</launch><param> 标签
<param> 标签用于在 ROS 参数服务器上设置参数。参数来源可在标签中直接指定,也可从外部文件加载。当 <param> 在 <node> 标签内部使用时,该参数被设置到节点的私有命名空间中。
1. 属性
-
name="namespace/parameter_name"
参数名,可包含命名空间。 -
value="xxx" (可选)
定义参数的值。如果省略,则必须指定外部文件作为参数来源。 -
type="str | int | double | bool | yaml" (可选)
指定参数的类型。如果未指定,roslaunch将根据值进行推断:- 含
.的数字解析为浮点数 (double)。 - 字符串 "true" 与 "false" 解析为布尔值(不区分大小写)。
- 其他内容解析为字符串。
- 含
2. 子标签
- 无
示例:
<launch>
<node name="node1" pkg="my_package" type="node_executable">
<param name="namespace/param_name" value="param_value" type="str"/>
</node>
</launch><rosparam> 标签
<rosparam> 标签允许从 YAML 文件加载参数、导出到 YAML 文件或删除参数。当用于 <node> 标签内部时,这些参数被视为私有的。
1. 属性
-
command="load | dump | delete" (可选,默认为
load)
指定要执行的操作:从文件加载参数、将其导出到文件,或删除它们。 -
file="$(find package_name)/path/to/file.yaml"
指定要加载或导出参数的 YAML 文件。 -
param="parameter_name"
参数名。 -
ns="namespace" (可选)
指定参数的命名空间。
2. 子标签
- 无
示例:
<launch>
<rosparam file="$(find my_package)/config/params.yaml" command="load" ns="my_namespace"/>
</launch><group> 标签
<group> 标签用于将节点和其他标签分组,允许对整个组应用命名空间或其他设置。
1. 属性
-
ns="namespace" (可选)
对组内所有节点和参数应用一个命名空间。 -
clear_params="true | false" (可选)
在组启动前清空该组命名空间内的所有参数。请谨慎使用,因为它会删除关键参数。
2. 子标签
- 除
<launch>标签外的任何标签都可以作为<group>的子标签。
示例:
<launch>
<group ns="my_namespace" clear_params="true">
<node name="node1" pkg="my_package" type="node_executable"/>
<node name="node2" pkg="my_package" type="node_executable"/>
</group>
</launch><arg> 标签
<arg> 标签用于定义可在运行时传入 launch 文件的动态参数,类似函数参数。它提高了 launch 文件的灵活性。
1. 属性
-
name="argument_name"
参数名。 -
default="default_value" (可选)
指定参数的默认值。 -
value="value" (可选)
指定参数值。不能与default同时使用。 -
doc="description"
参数说明。
2. 子标签
- 无
3. 示例
带参数语法的 launch 文件,hello.launch:
<launch>
<arg name="robot_name" default="my_robot"/>
<param name="robot_name" value="$(arg robot_name)"/>
</launch>通过命令行传递参数:
roslaunch hello.launch robot_name:=robot_valueROS 工作空间覆盖
设想您有两个自定义工作空间 Workspace A 和 Workspace B,均包含名为 turtlesim 的软件包。此外,系统内置的工作空间也有名为 turtlesim 的软件包。当您调用 turtlesim 时,会使用哪一个?
实现步骤
步骤 0:创建工作空间 A 和 B
首先,创建两个独立的工作空间 A 和 B。在每个工作空间中创建一个名为 turtlesim 的软件包。
步骤 1:修改 ~/.bashrc 文件
在 ~/.bashrc 文件中加入以下行,以 source 两个工作空间的 setup 文件:
source /home/user/path/to/workspaceA/devel/setup.bash
source /home/user/path/to/workspaceB/devel/setup.bash请将 /home/user/path/to/ 替换为您的实际工作空间路径。
步骤 2:加载环境变量
打开新的终端,运行以下命令以加载更新的环境变量:
source ~/.bashrc步骤 3:检查 ROS 环境变量
要查看 ROS 软件包的路径,运行:
echo $ROS_PACKAGE_PATH结果: 输出将按如下顺序显示路径:Workspace B → Workspace A → 系统内置工作空间。
步骤 4:调用 turtlesim 软件包
现在运行以下命令进入 turtlesim 软件包:
roscd turtlesim结果: 您将进入 Workspace B 中的 turtlesim 软件包。
处理 ROS 节点名称冲突
场景
在 ROS 中,每个节点都有一个名称,该名称在节点初始化时定义。在 C++ 中通过 ros::init(argc, argv, "node_name"); API 设置,在 Python 中通过 rospy.init_node("node_name")。在 ROS 网络拓扑中,节点必须具有唯一名称,因为多个节点共享同一名称会在调用时引起混乱。具体来说,如果启动一个名称重复的节点,已存在的同名节点会被自动关闭。但如果您需要运行同一节点的多个实例,或处理名称冲突怎么办?
ROS 提供了两种策略来处理这种情况:命名空间 和 名称重映射。
- 命名空间 在节点名称前添加前缀。
- 名称重映射 给节点名称起一个别名。
两种策略都可以解决节点名称冲突,且可以通过多种方式实现:
- 使用
rosrun命令。 - 通过 launch 文件。
- 在节点的代码中。
本节将演示如何使用这三种方法避免节点名称冲突。
示例场景
让我们启动两个 turtlesim_node 节点。如果您打开两个终端并直接启动节点而不做更改,启动第二个节点时第一个会被关闭。您会看到警告:
[ WARN] [1578812836.351049332]: Shutdown request received.
[ WARN] [1578812836.351207362]: Reason given for shutdown: [new node registered with same name]由于节点不能共享相同名称,我们将探讨几种策略来解决此问题。
使用 rosrun 设置命名空间和重映射
1. 使用 rosrun 设置命名空间
您可以使用以下语法为节点设置命名空间:
rosrun package_name node_name __ns:=/new_namespace示例:
rosrun turtlesim turtlesim_node __ns:=/xxx
rosrun turtlesim turtlesim_node __ns:=/yyy通过这些命令,两个节点都能正常运行。
结果:
使用 rosnode list 检查节点:
/xxx/turtlesim
/yyy/turtlesim2. 使用 rosrun 重映射节点名称
您也可以重映射节点的名称,实质上是给它一个别名,语法如下:
rosrun package_name node_name __name:=new_name示例:
rosrun turtlesim turtlesim_node __name:=t1
rosrun turtlesim turtlesim_node __name:=t2通过这些命令,两个节点都将以新名称运行。
结果:
使用 rosnode list 检查节点:
/t1
/t23. 使用 rosrun 同时设置命名空间和重映射
您可以同时使用这两种技巧,既设置命名空间又重映射节点名称:
rosrun package_name node_name __ns:=/new_namespace __name:=new_name示例:
rosrun turtlesim turtlesim_node __ns:=/xxx __name:=tn结果:
使用 rosnode list 检查节点:
/xxx/tn或者,您可以在启动节点之前通过环境变量设置命名空间:
export ROS_NAMESPACE=xxxx使用 launch 文件设置命名空间和重映射
在 launch 文件中,<node> 标签包含两个重要属性:name 和 ns。它们分别用于名称重映射和设置命名空间。使用 launch 文件处理命名空间和名称重映射非常简单。
1. launch 文件示例
下面演示如何在 launch 文件中设置命名空间和名称重映射:
<launch>
<node pkg="turtlesim" type="turtlesim_node" name="t1" />
<node pkg="turtlesim" type="turtlesim_node" name="t2" />
<node pkg="turtlesim" type="turtlesim_node" name="t1" ns="hello"/>
</launch>在该示例中,name 属性是必需的,而 ns 是可选的。
2. 运行 launch 文件
运行 launch 文件,然后使用 rosnode list 查看结果:
/t1
/t2
/hello/t1在代码中设置命名空间与重映射
如果您实现自定义节点,在代码中设置命名空间和名称重映射会更加灵活。
1. C++ 实现:名称重映射
您可以使用以下代码设置名称别名:
ros::init(argc, argv, "zhangsan", ros::init_options::AnonymousName);执行:
该方式会在节点名称后追加时间戳,确保其唯一。
2. C++ 实现:设置命名空间
您可以直接在代码中设置命名空间,如下所示:
std::map<std::string, std::string> map;
map["__ns"] = "xxxx";
ros::init(map, "wangqiang");执行:
这会为节点设置一个命名空间,使其能够无冲突运行。
3. Python 实现:名称重映射
在 Python 中,您可以通过以下代码实现类似功能:
rospy.init_node("lisi", anonymous=True)ROS 中的话题名称重映射
在 ROS 中,话题名称重映射允许您在不修改节点代码的情况下更改节点订阅或发布的话题名称。这在集成多个需要通过不同话题名称通信的节点时非常有用。在 ROS 中重映射话题名称主要有三种方法:
- 使用
rosrun命令。 - 通过 launch 文件。
- 在 C++ 或 Python 中直接修改代码。
使用 rosrun 重映射话题
使用 rosrun 重映射话题名称的语法是:
rosrun package_name node_name old_topic_name:=new_topic_name示例:将 teleop_twist_keyboard 与 turtlesim 集成
在 teleop_twist_keyboard 节点和 turtlesim 显示节点之间建立通信有两种方式:
1. 方案 1:重映射 teleop_twist_keyboard 话题
在该方式中,我们将 teleop_twist_keyboard 节点的话题重映射到 /turtle1/cmd_vel。
-
启动键盘控制节点:
bashrosrun teleop_twist_keyboard teleop_twist_keyboard.py /cmd_vel:=/turtle1/cmd_vel -
启动 turtlesim 显示节点:
bashrosrun turtlesim turtlesim_node
两个节点将通过 /turtle1/cmd_vel 话题正确通信。
2. 方案 2:重映射 turtlesim 话题
或者,我们可以将 turtlesim 节点的话题重映射到 /cmd_vel。
-
启动键盘控制节点:
bashrosrun teleop_twist_keyboard teleop_twist_keyboard.py -
启动 turtlesim 显示节点:
bashrosrun turtlesim turtlesim_node /turtle1/cmd_vel:=/cmd_vel
两个节点将通过 /cmd_vel 话题正确通信。
使用 launch 文件重映射话题
您也可以在 launch 文件中重映射话题。在 launch 文件中重映射话题的语法是:
<node pkg="package_name" type="node_type" name="node_name">
<remap from="original_topic" to="new_topic" />
</node>示例:使用 launch 文件将 teleop_twist_keyboard 与 turtlesim 集成
同样有两种方案:
1. 方案 1:重映射 teleop_twist_keyboard 话题
在该方式中,我们将 teleop_twist_keyboard 节点的话题重映射到 /turtle1/cmd_vel。
<launch>
<node pkg="turtlesim" type="turtlesim_node" name="t1" />
<node pkg="teleop_twist_keyboard" type="teleop_twist_keyboard.py" name="key">
<remap from="/cmd_vel" to="/turtle1/cmd_vel" />
</node>
</launch>两个节点将正确通信。
2. 方案 2:重映射 turtlesim 话题
在该方式中,我们将 turtlesim 节点的话题重映射到 /cmd_vel。
<launch>
<node pkg="turtlesim" type="turtlesim_node" name="t1">
<remap from="/turtle1/cmd_vel" to="/cmd_vel" />
</node>
<node pkg="teleop_twist_keyboard" type="teleop_twist_keyboard.py" name="key" />
</launch>两个节点将正确通信。
在代码中重映射话题
ROS 中的话题名称受节点命名空间、节点名称以及话题自身名称的影响。话题名称大致可分为三类:
- 全局: 话题名称是绝对的,以
/开头,与节点的命名空间无关。 - 相对: 话题名称是相对的,不以
/开头,即在节点命名空间内解析。 - 私有: 话题名称是私有的,以
~开头,即相对于节点的私有命名空间解析。
我们通过 C++ 与 Python 的示例来探讨这些概念。
1. C++ 实现
示例准备:
-
使用名称初始化节点:
cppros::init(argc, argv, "hello"); -
设置不同类型的话题名称。
-
启动节点时传递
__ns:=xxx参数。 -
节点启动后,使用
rostopic检查话题信息。
全局话题名称
全局话题名称以 / 开头,与节点名称或命名空间无关。
-
示例 1:
cppros::Publisher pub = nh.advertise<std_msgs::String>("/chatter", 1000);结果:
/chatter -
示例 2:
cppros::Publisher pub = nh.advertise<std_msgs::String>("/chatter/money", 1000);结果:
/chatter/money
相对话题名称
相对话题名称不以 / 开头,相对于节点的命名空间进行解析。
-
示例 1:
cppros::Publisher pub = nh.advertise<std_msgs::String>("chatter", 1000);结果:
xxx/chatter -
示例 2:
cppros::Publisher pub = nh.advertise<std_msgs::String>("chatter/money", 1000);结果:
xxx/chatter/money
私有话题名称
私有话题名称以 ~ 开头,相对于节点的私有命名空间进行解析。
-
示例 1:
cppros::NodeHandle nh("~"); ros::Publisher pub = nh.advertise<std_msgs::String>("chatter", 1000);结果:
/xxx/hello/chatter -
示例 2:
cppros::NodeHandle nh("~"); ros::Publisher pub = nh.advertise<std_msgs::String>("chatter/money", 1000);结果:
/xxx/hello/chatter/money -
特殊情况: 在使用
~时,如果话题名称以/开头,则会被视为绝对名称。cppros::NodeHandle nh("~"); ros::Publisher pub = nh.advertise<std_msgs::String>("/chatter/money", 1000);结果:
/chatter/money
Python 实现
示例准备:
-
使用名称初始化节点:
pythonrospy.init_node("hello") -
设置不同类型的话题名称。
-
启动节点时传递
__ns:=xxx参数。 -
节点启动后,使用
rostopic检查话题信息。
全局话题名称
全局话题名称以 / 开头,与节点名称或命名空间无关。
-
示例 1:
pythonpub = rospy.Publisher("/chatter", String, queue_size=1000)结果:
/chatter -
示例 2:
pythonpub = rospy.Publisher("/chatter/money", String, queue_size=1000)结果:
/chatter/money
相对话题名称
相对话题名称不以 / 开头,相对于节点的命名空间进行解析。
-
示例 1:
pythonpub = rospy.Publisher("chatter", String, queue_size=1000)结果:
xxx/chatter -
示例 2:
pythonpub = rospy.Publisher("chatter/money", String, queue_size=1000)结果:
xxx/chatter/money
私有话题名称
私有话题名称以 ~ 开头,相对于节点的私有命名空间进行解析。
-
示例 1:
pythonpub = rospy.Publisher("~chatter", String, queue_size=1000)结果:
/xxx/hello/chatter -
示例 2:
pythonpub = rospy.Publisher("~chatter/money", String, queue_size=1000)结果:
/xxx/hello/chatter/money
在 ROS 中设置参数
在 ROS 中,参数用于在运行时配置节点。可以通过多种方式设置:使用 rosrun 命令、在 launch 文件中或直接在代码中。参数可以是全局的、相对的或私有的,取决于其定义方式。
使用 rosrun 设置参数
您可以在使用 rosrun 命令启动节点时设置参数。设置参数的语法为:
rosrun package_name node_name _parameter_name:=parameter_value示例:为 Turtlesim 节点设置参数
让我们启动 turtlesim_node 并设置参数 A = 100。
rosrun turtlesim turtlesim_node _A:=100检查参数
您可以使用以下命令列出所有参数并查看结果:
rosparam list输出:
/turtlesim/A
/turtlesim/background_b
/turtlesim/background_g
/turtlesim/background_r说明: 参数 A 前缀加上了节点名(/turtlesim/),表明使用 rosrun 设置参数时,采用的是私有命名空间方式。
在 launch 文件中设置参数
如前所述,可以在 launch 文件中通过 <param> 或 <rosparam> 标签设置参数。在 <node> 标签外部设置的参数是全局的,而在 <node> 标签内部设置的参数是私有的,相对于节点的命名空间。
示例:使用 <param> 标签设置参数
下面的示例分别设置一个全局参数和一个私有参数:
<launch>
<param name="p1" value="100" />
<node pkg="turtlesim" type="turtlesim_node" name="t1">
<param name="p2" value="100" />
</node>
</launch>检查参数
运行 launch 文件后,可使用以下命令检查参数:
rosparam list输出:
/p1
/t1/p2说明: 参数 p1 是全局的,而 p2 是 t1 节点的私有参数,这一点从命名空间可以看出。
在代码中设置参数
在代码中设置参数提供了更大的灵活性,允许以编程方式定义全局、相对和私有参数。
1. C++ 实现
在 C++ 中,可使用 ros::param API 或通过 ros::NodeHandle 对象设置参数。
1.1 使用 ros::param 设置参数
ros::param::set 函数用于设置参数。该函数的第一个参数是参数名,第二个是参数值。如果参数名以 / 开头,则为全局参数;若以 ~ 开头,则为私有参数;否则为相对参数。
示例:
ros::param::set("/set_A", 100); // Global, independent of namespace and node name
ros::param::set("set_B", 100); // Relative, dependent on namespace
ros::param::set("~set_C", 100); // Private, dependent on namespace and node name假设命名空间为 xxx,节点名为 yyy,使用 rosparam list 检查参数将显示:
/set_A
/xxx/set_B
/xxx/yyy/set_C使用 ros::NodeHandle 设置参数
要使用 ros::NodeHandle 设置参数,首先创建一个 NodeHandle 对象,然后调用 setParam 方法。如果参数名以 / 开头,则为全局参数;如果不是以 / 开头,具体是相对还是私有取决于 NodeHandle 对象的创建方式。
示例:
ros::NodeHandle nh;
nh.setParam("/nh_A", 100); // Global, independent of namespace and node name
nh.setParam("nh_B", 100); // Relative, dependent on namespace
ros::NodeHandle nh_private("~");
nh_private.setParam("nh_C", 100); // Private, dependent on namespace and node name假设命名空间为 xxx,节点名为 yyy,使用 rosparam list 检查参数将显示:
/nh_A
/xxx/nh_B
/xxx/yyy/nh_C2. Python 实现
在 Python 中,设置参数比 C++ 略简单。rospy.set_param 函数用于设置参数。第一个参数为参数名,第二个为参数值。与 C++ 一样,若参数名以 / 开头则为全局,若以 ~ 开头则为私有,否则为相对参数。
示例:
rospy.set_param("/py_A", 100) # Global, independent of namespace and node name
rospy.set_param("py_B", 100) # Relative, dependent on namespace
rospy.set_param("~py_C", 100) # Private, dependent on namespace and node name假设命名空间为 xxx,节点名为 yyy,使用 rosparam list 检查参数将显示:
/py_A
/xxx/py_B
/xxx/yyy/py_C