7.1.6-ROS 运行管理

使用 launch 文件管理 ROS 节点

ROS 中的 launch 文件是 XML 格式的文件,用于高效地启动和管理多个 ROS 节点。本节介绍 launch 文件中可用的各种标签,包括它们的属性和用例。

<launch> 标签

<launch> 标签是每个 launch 文件的根标签,作为所有其他标签的容器。

1. 属性

  • deprecated="弃用说明"
    向用户提示当前 launch 文件已被弃用。

2. 子标签

  • launch 文件中所有其他标签都是 <launch> 标签的子元素。

示例:

xml
<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: 用于设置参数。

示例:

xml
<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 文件传递参数。

示例:

xml
<launch>
    <include file="$(find my_package)/launch/another_launch_file.launch" ns="my_namespace"/>
</launch>

<remap> 标签

<remap> 标签用于重映射 ROS 话题或服务名。在避免名称冲突或在不同节点间统一命名时很有用。

1. 属性

  • from="xxx"
    原始的话题或服务名。

  • to="yyy"
    话题或服务的新名称。

2. 子标签

示例:

xml
<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. 子标签

示例:

xml
<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. 子标签

示例:

xml
<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> 的子标签。

示例:

xml
<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:

xml
<launch>
    <arg name="robot_name" default="my_robot"/>
    <param name="robot_name" value="$(arg robot_name)"/>
</launch>

通过命令行传递参数:

bash
roslaunch hello.launch robot_name:=robot_value

ROS 工作空间覆盖

设想您有两个自定义工作空间 Workspace A 和 Workspace B,均包含名为 turtlesim 的软件包。此外,系统内置的工作空间也有名为 turtlesim 的软件包。当您调用 turtlesim 时,会使用哪一个?

实现步骤

步骤 0:创建工作空间 A 和 B

首先,创建两个独立的工作空间 A 和 B。在每个工作空间中创建一个名为 turtlesim 的软件包。

步骤 1:修改 ~/.bashrc 文件

~/.bashrc 文件中加入以下行,以 source 两个工作空间的 setup 文件:

bash
source /home/user/path/to/workspaceA/devel/setup.bash
source /home/user/path/to/workspaceB/devel/setup.bash

请将 /home/user/path/to/ 替换为您的实际工作空间路径。

步骤 2:加载环境变量

打开新的终端,运行以下命令以加载更新的环境变量:

bash
source ~/.bashrc

步骤 3:检查 ROS 环境变量

要查看 ROS 软件包的路径,运行:

bash
echo $ROS_PACKAGE_PATH

结果: 输出将按如下顺序显示路径:Workspace B → Workspace A → 系统内置工作空间。

步骤 4:调用 turtlesim 软件包

现在运行以下命令进入 turtlesim 软件包:

bash
roscd turtlesim

结果: 您将进入 Workspace B 中的 turtlesim 软件包。

处理 ROS 节点名称冲突

场景

在 ROS 中,每个节点都有一个名称,该名称在节点初始化时定义。在 C++ 中通过 ros::init(argc, argv, "node_name"); API 设置,在 Python 中通过 rospy.init_node("node_name")。在 ROS 网络拓扑中,节点必须具有唯一名称,因为多个节点共享同一名称会在调用时引起混乱。具体来说,如果启动一个名称重复的节点,已存在的同名节点会被自动关闭。但如果您需要运行同一节点的多个实例,或处理名称冲突怎么办?

ROS 提供了两种策略来处理这种情况:命名空间名称重映射

  • 命名空间 在节点名称前添加前缀。
  • 名称重映射 给节点名称起一个别名。

两种策略都可以解决节点名称冲突,且可以通过多种方式实现:

  1. 使用 rosrun 命令。
  2. 通过 launch 文件。
  3. 在节点的代码中。

本节将演示如何使用这三种方法避免节点名称冲突。

示例场景

让我们启动两个 turtlesim_node 节点。如果您打开两个终端并直接启动节点而不做更改,启动第二个节点时第一个会被关闭。您会看到警告:

plaintext
[ WARN] [1578812836.351049332]: Shutdown request received.
[ WARN] [1578812836.351207362]: Reason given for shutdown: [new node registered with same name]

由于节点不能共享相同名称,我们将探讨几种策略来解决此问题。

使用 rosrun 设置命名空间和重映射

1. 使用 rosrun 设置命名空间

您可以使用以下语法为节点设置命名空间:

bash
rosrun package_name node_name __ns:=/new_namespace

示例:

bash
rosrun turtlesim turtlesim_node __ns:=/xxx
rosrun turtlesim turtlesim_node __ns:=/yyy

通过这些命令,两个节点都能正常运行。

结果:

使用 rosnode list 检查节点:

plaintext
/xxx/turtlesim
/yyy/turtlesim

2. 使用 rosrun 重映射节点名称

您也可以重映射节点的名称,实质上是给它一个别名,语法如下:

bash
rosrun package_name node_name __name:=new_name

示例:

bash
rosrun turtlesim turtlesim_node __name:=t1
rosrun turtlesim turtlesim_node __name:=t2

通过这些命令,两个节点都将以新名称运行。

结果:

使用 rosnode list 检查节点:

plaintext
/t1
/t2

3. 使用 rosrun 同时设置命名空间和重映射

您可以同时使用这两种技巧,既设置命名空间又重映射节点名称:

bash
rosrun package_name node_name __ns:=/new_namespace __name:=new_name

示例:

bash
rosrun turtlesim turtlesim_node __ns:=/xxx __name:=tn

结果:

使用 rosnode list 检查节点:

plaintext
/xxx/tn

或者,您可以在启动节点之前通过环境变量设置命名空间:

bash
export ROS_NAMESPACE=xxxx

使用 launch 文件设置命名空间和重映射

在 launch 文件中,<node> 标签包含两个重要属性:namens。它们分别用于名称重映射和设置命名空间。使用 launch 文件处理命名空间和名称重映射非常简单。

1. launch 文件示例

下面演示如何在 launch 文件中设置命名空间和名称重映射:

xml
<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 查看结果:

plaintext
/t1
/t2
/hello/t1

在代码中设置命名空间与重映射

如果您实现自定义节点,在代码中设置命名空间和名称重映射会更加灵活。

1. C++ 实现:名称重映射

您可以使用以下代码设置名称别名:

cpp
ros::init(argc, argv, "zhangsan", ros::init_options::AnonymousName);

执行:

该方式会在节点名称后追加时间戳,确保其唯一。

2. C++ 实现:设置命名空间

您可以直接在代码中设置命名空间,如下所示:

cpp
std::map<std::string, std::string> map;
map["__ns"] = "xxxx";
ros::init(map, "wangqiang");

执行:

这会为节点设置一个命名空间,使其能够无冲突运行。

3. Python 实现:名称重映射

在 Python 中,您可以通过以下代码实现类似功能:

python
rospy.init_node("lisi", anonymous=True)

ROS 中的话题名称重映射

在 ROS 中,话题名称重映射允许您在不修改节点代码的情况下更改节点订阅或发布的话题名称。这在集成多个需要通过不同话题名称通信的节点时非常有用。在 ROS 中重映射话题名称主要有三种方法:

  1. 使用 rosrun 命令。
  2. 通过 launch 文件。
  3. 在 C++ 或 Python 中直接修改代码。

使用 rosrun 重映射话题

使用 rosrun 重映射话题名称的语法是:

bash
rosrun package_name node_name old_topic_name:=new_topic_name

示例:将 teleop_twist_keyboardturtlesim 集成

teleop_twist_keyboard 节点和 turtlesim 显示节点之间建立通信有两种方式:

1. 方案 1:重映射 teleop_twist_keyboard 话题

在该方式中,我们将 teleop_twist_keyboard 节点的话题重映射到 /turtle1/cmd_vel

  • 启动键盘控制节点:

    bash
    rosrun teleop_twist_keyboard teleop_twist_keyboard.py /cmd_vel:=/turtle1/cmd_vel
  • 启动 turtlesim 显示节点:

    bash
    rosrun turtlesim turtlesim_node

两个节点将通过 /turtle1/cmd_vel 话题正确通信。

2. 方案 2:重映射 turtlesim 话题

或者,我们可以将 turtlesim 节点的话题重映射到 /cmd_vel

  • 启动键盘控制节点:

    bash
    rosrun teleop_twist_keyboard teleop_twist_keyboard.py
  • 启动 turtlesim 显示节点:

    bash
    rosrun turtlesim turtlesim_node /turtle1/cmd_vel:=/cmd_vel

两个节点将通过 /cmd_vel 话题正确通信。

使用 launch 文件重映射话题

您也可以在 launch 文件中重映射话题。在 launch 文件中重映射话题的语法是:

xml
<node pkg="package_name" type="node_type" name="node_name">
    <remap from="original_topic" to="new_topic" />
</node>

示例:使用 launch 文件将 teleop_twist_keyboardturtlesim 集成

同样有两种方案:

1. 方案 1:重映射 teleop_twist_keyboard 话题

在该方式中,我们将 teleop_twist_keyboard 节点的话题重映射到 /turtle1/cmd_vel

xml
<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

xml
<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 中的话题名称受节点命名空间、节点名称以及话题自身名称的影响。话题名称大致可分为三类:

  1. 全局: 话题名称是绝对的,以 / 开头,与节点的命名空间无关。
  2. 相对: 话题名称是相对的,不以 / 开头,即在节点命名空间内解析。
  3. 私有: 话题名称是私有的,以 ~ 开头,即相对于节点的私有命名空间解析。

我们通过 C++ 与 Python 的示例来探讨这些概念。

1. C++ 实现

示例准备:

  1. 使用名称初始化节点:

    cpp
    ros::init(argc, argv, "hello");
  2. 设置不同类型的话题名称。

  3. 启动节点时传递 __ns:=xxx 参数。

  4. 节点启动后,使用 rostopic 检查话题信息。

全局话题名称

全局话题名称以 / 开头,与节点名称或命名空间无关。

  • 示例 1:

    cpp
    ros::Publisher pub = nh.advertise<std_msgs::String>("/chatter", 1000);

    结果: /chatter

  • 示例 2:

    cpp
    ros::Publisher pub = nh.advertise<std_msgs::String>("/chatter/money", 1000);

    结果: /chatter/money

相对话题名称

相对话题名称不以 / 开头,相对于节点的命名空间进行解析。

  • 示例 1:

    cpp
    ros::Publisher pub = nh.advertise<std_msgs::String>("chatter", 1000);

    结果: xxx/chatter

  • 示例 2:

    cpp
    ros::Publisher pub = nh.advertise<std_msgs::String>("chatter/money", 1000);

    结果: xxx/chatter/money

私有话题名称

私有话题名称以 ~ 开头,相对于节点的私有命名空间进行解析。

  • 示例 1:

    cpp
    ros::NodeHandle nh("~");
    ros::Publisher pub = nh.advertise<std_msgs::String>("chatter", 1000);

    结果: /xxx/hello/chatter

  • 示例 2:

    cpp
    ros::NodeHandle nh("~");
    ros::Publisher pub = nh.advertise<std_msgs::String>("chatter/money", 1000);

    结果: /xxx/hello/chatter/money

  • 特殊情况: 在使用 ~ 时,如果话题名称以 / 开头,则会被视为绝对名称。

    cpp
    ros::NodeHandle nh("~");
    ros::Publisher pub = nh.advertise<std_msgs::String>("/chatter/money", 1000);

    结果: /chatter/money

Python 实现

示例准备:

  1. 使用名称初始化节点:

    python
    rospy.init_node("hello")
  2. 设置不同类型的话题名称。

  3. 启动节点时传递 __ns:=xxx 参数。

  4. 节点启动后,使用 rostopic 检查话题信息。

全局话题名称

全局话题名称以 / 开头,与节点名称或命名空间无关。

  • 示例 1:

    python
    pub = rospy.Publisher("/chatter", String, queue_size=1000)

    结果: /chatter

  • 示例 2:

    python
    pub = rospy.Publisher("/chatter/money", String, queue_size=1000)

    结果: /chatter/money

相对话题名称

相对话题名称不以 / 开头,相对于节点的命名空间进行解析。

  • 示例 1:

    python
    pub = rospy.Publisher("chatter", String, queue_size=1000)

    结果: xxx/chatter

  • 示例 2:

    python
    pub = rospy.Publisher("chatter/money", String, queue_size=1000)

    结果: xxx/chatter/money

私有话题名称

私有话题名称以 ~ 开头,相对于节点的私有命名空间进行解析。

  • 示例 1:

    python
    pub = rospy.Publisher("~chatter", String, queue_size=1000)

    结果: /xxx/hello/chatter

  • 示例 2:

    python
    pub = rospy.Publisher("~chatter/money", String, queue_size=1000)

    结果: /xxx/hello/chatter/money


在 ROS 中设置参数

在 ROS 中,参数用于在运行时配置节点。可以通过多种方式设置:使用 rosrun 命令、在 launch 文件中或直接在代码中。参数可以是全局的、相对的或私有的,取决于其定义方式。

使用 rosrun 设置参数

您可以在使用 rosrun 命令启动节点时设置参数。设置参数的语法为:

bash
rosrun package_name node_name _parameter_name:=parameter_value

示例:为 Turtlesim 节点设置参数

让我们启动 turtlesim_node 并设置参数 A = 100

bash
rosrun turtlesim turtlesim_node _A:=100

检查参数

您可以使用以下命令列出所有参数并查看结果:

bash
rosparam list

输出:

plaintext
/turtlesim/A
/turtlesim/background_b
/turtlesim/background_g
/turtlesim/background_r

说明: 参数 A 前缀加上了节点名(/turtlesim/),表明使用 rosrun 设置参数时,采用的是私有命名空间方式。

在 launch 文件中设置参数

如前所述,可以在 launch 文件中通过 <param><rosparam> 标签设置参数。在 <node> 标签外部设置的参数是全局的,而在 <node> 标签内部设置的参数是私有的,相对于节点的命名空间。

示例:使用 <param> 标签设置参数

下面的示例分别设置一个全局参数和一个私有参数:

xml
<launch>
    <param name="p1" value="100" />
    <node pkg="turtlesim" type="turtlesim_node" name="t1">
        <param name="p2" value="100" />
    </node>
</launch>

检查参数

运行 launch 文件后,可使用以下命令检查参数:

bash
rosparam list

输出:

plaintext
/p1
/t1/p2

说明: 参数 p1 是全局的,而 p2t1 节点的私有参数,这一点从命名空间可以看出。

在代码中设置参数

在代码中设置参数提供了更大的灵活性,允许以编程方式定义全局、相对和私有参数。

1. C++ 实现

在 C++ 中,可使用 ros::param API 或通过 ros::NodeHandle 对象设置参数。

1.1 使用 ros::param 设置参数

ros::param::set 函数用于设置参数。该函数的第一个参数是参数名,第二个是参数值。如果参数名以 / 开头,则为全局参数;若以 ~ 开头,则为私有参数;否则为相对参数。

示例:

cpp
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 检查参数将显示:

plaintext
/set_A
/xxx/set_B
/xxx/yyy/set_C

使用 ros::NodeHandle 设置参数

要使用 ros::NodeHandle 设置参数,首先创建一个 NodeHandle 对象,然后调用 setParam 方法。如果参数名以 / 开头,则为全局参数;如果不是以 / 开头,具体是相对还是私有取决于 NodeHandle 对象的创建方式。

示例:

cpp
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 检查参数将显示:

plaintext
/nh_A
/xxx/nh_B
/xxx/yyy/nh_C

2. Python 实现

在 Python 中,设置参数比 C++ 略简单。rospy.set_param 函数用于设置参数。第一个参数为参数名,第二个为参数值。与 C++ 一样,若参数名以 / 开头则为全局,若以 ~ 开头则为私有,否则为相对参数。

示例:

python
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 检查参数将显示:

plaintext
/py_A
/xxx/py_B
/xxx/yyy/py_C