在ROS 2项目开发过程中,我们经常会遇到项目配置管理上的一些问题。例如,调整相机的分辨率参数时,需要找到对应代码进行修改并重新编译;优化算法中的关键阈值修改后,必须重新启动整个节点才能让新参数生效;当多个节点需要共用机器人名称、坐标系前缀等同一配置信息时,需要在每个节点的代码或配置文件中重复定义,这不仅会造成代码冗余,还容易出现配置不一致的问题。为了解决这些类似问题,ROS 2专门设计了“参数服务器”这一核心组件。

本节将介绍ROS 2参数服务器模型,以及分别使用C++和Python实现ROS 2参数操作的实例与相关配置操作。

4.2.1  参数服务器模型介绍

ROS 2 的参数服务器并不是一个独立节点,而是集成在每个节点中的参数存储模块。通过参数服务器,每个节点可以管理自己的私有参数,也可以通过节点名称访问其他节点的参数。例如,每个ROS 2节点就像一部手机,参数是手机中的亮度、音量等设置项目,参数服务器则是手机的设置面板。本小节将介绍ROS 2参数服务器模型,以及参数通信、话题通信、服务通信之间的区别与使用场景等相关知识。

1. ROS 2参数通信模式

为了方便我们更直观地理解ROS 2参数通信模式,这里使用手机参数设置来类比参数服务器的工作模式,其对应关系如表4.3所示。

表4.3  ROS 2参数服务与手机参数设置比较

手机设置场景

ROS 2参数服务器

描    述

手机

节点

管理节点参数

设置项目

参数键(Key)

参数的唯一标识

设置项目的值

参数值(Value)

参数的具体内容

查看/修改设置值

参数操作的API

节点通过API读取/修改参数

系统设置

全局参数

所有节点可访问的参数,需要通过“全局节点”进行管理

根据ROS 2参数服务器与手机设置场景的比较,ROS 2参数服务具有以下几个特性。

(1)动态性:修改参数后无须重启节点。例如,手机修改屏幕亮度后,不用重启手机,设置直接生效。

(2)私有性:默认参数属于节点私有,其他节点无法访问。例如,手机A的亮度设置不会影响手机B的屏幕亮度。

(3)类型安全性:参数值必须与参数声明类型一致。例如,屏幕亮度数值类型为int,那么该数值就不能为"abc"。

在前面的章节中,已经向读者讲解了ROS 2话题通信、服务通信的交互模式与应用场景。为了区分ROS 2参数与前两种通信模式的区别与应用场景,我们将对这三种通信模式的特点以及适用场景等分别进行说明,如表4.4所示。

表4.4  ROS 2话题通信、服务通信以及参数通信模式的对比

组    件

数据形式

交互模式

核心作用

适用场景

话题通信

持续的数据流

单向异步

传输实时数据

相机图像、激光雷达点云

服务通信

请求+响应

双向同步

执行需确认的任务

路径规划请求、设备控制命令

参数通信

静态/动态的键值对

读写操作

存储配置信息

相机分辨率、算法阈值、机器人名字

表4.4对于ROS 2中的话题通信、服务通信以及参数通信三种通信模式进行了比较。这里再通过列举机器人应用场景,对以上三种通信模式的功能与适用场景进行深入理解,说明如下:

(1)相机节点使用话题通信发送实时图像等连续数据。

(2)控制节点使用服务通信请求机械臂抓取并获取响应。

(3)相机节点使用参数通信存储分辨率等配置信息并支持动态修改。

2. 参数的核心操作与规则

我们在实际项目开发中,常用的参数操作有4种并且需要遵守对应的操作规则,具体说明如下。

(1)声明参数(Declare):首次使用参数前需要进行声明,指定其参数、默认值、类型等。例如,declare_parameter("resolution", "1920x1080"),与C/C++语言中的函数声明基本一致。其操作规则是未声明的参数不能直接读写,否则会报Parameter not declared错误。

(2)获取参数(Get):读取参数值。例如,get_parameter("resolution")。其规则是:如果参数未设置值,将会返回参数声明时的默认值。

(3)设置参数(Set):设置、修改参数值。例如,set_parameter(Parameter("resolution", "1280x720"))。其规则是设置、修改的参数值类型必须与声明的类型一致。例如,声明为string类型的参数不能被设置或者修改为int类型。

(4)删除参数(Delete):删除已声明的参数。例如,delete_parameter("resolution")。其规则是参数被删除后,如需再次使用,必须重新声明。

本小节主要介绍了ROS 2参数服务器的核心内容并用手机设置进行了类比,并说明了参数服务的动态性、私有性、类型安全性等特性。同时,对比了参数、话题与服务的通信模式的差异,明确参数以“静态/动态键值对”实现读写交互以及适用场景等,并且讲解了在实际开发过程中参数的四大核心操作及规则。

通过本小节的学习,我们能够对ROS 2参数通信服务器的基本概念和作用有更为直观的理解,为后面的实例编写打下坚实的基础。

4.2.2  C++实现参数操作

在ROS 2中使用C++进行开发时,rclcpp::Node类提供了相应的API来支持C++节点实现参数的声明、获取、设置和删除。本小节将以相机节点camera_node为例,介绍ROS 2参数通信中的C++代码编写、编译、配置以及运行等基础操作。

1. 核心API

在rclcpp::Node类中,已经为我们开发ROS 2参数通信提供了常用的API函数,具体如表4.5所示。

表4.5  rclcpp::Node类中常用的参数操作API

API函数原型

功能及参数

以分辨率参数为示例

void declare_parameter<T>(const std::string &name, const T &default_value)

声明参数,指定参数名、类型、默认值

declare_parameter<std::string>("resolution", "1920x1080")

bool get_parameter(const std::string &name, T &value)

获取参数值,并保存在参数value中

std::string res;

get_parameter("resolution", res);

rclcpp::Parameter get_parameter(const std::string &name)

获取参数对象,包括参数名、参数值及其类型

auto param = get_parameter("resolution");

bool set_parameter(const rclcpp::Parameter &parameter)

设置参数值,参数类型为Parameter

set_parameter(rclcpp::Parameter("resolution", "1280x720"))

bool delete_parameter(const std::string &name)

删除参数

delete_parameter("resolution");

bool has_parameter(const std::string &name)

检查参数是否存在

if (has_parameter("resolution"))

{ ... }

表4.5给出了rclcpp::Node类中关于ROS 2参数通信的常用API函数功能及其参数含义,并以分辨率参数为例列举了示例代码。

2. C++参数操作实现

接下来,使用示例“相机参数节点”具体讲解参数通信的功能实现。在该示例中,声明4个参数及其类型,分别为分辨率(string)、帧率(int)、自动曝光(bool)、曝光时间(double),功能包括每三秒定时打印输出一次以上参数值,并且支持动态修改参数,修改后的打印输出信息也将随之更新。

在4.2.1节中创建的功能包service_paramadd_demo下的目录src中,新建一个C++源文件param_demo.cpp作为相机参数节点,代码如下所示:

// 包含头文件

#include "rclcpp/rclcpp.hpp"

#include "rclcpp/parameter.hpp"    // 参数相关类

#include "rclcpp/logging.hpp"      // 日志功能头文件

#include <chrono>                    // 用于定时

// 定义参数节点类(继承自rclcpp::Node)

class CameraParamNode : public rclcpp::Node

{

public:

    // 构造函数(初始化节点+声明参数+定时打印)

    CameraParamNode() : Node("camera_param_node")

    {

        // 声明参数(带默认值,类型自动推导)

        // 字符串参数:相机分辨率(默认为1920×1080)

        this->declare_parameter<std::string>("resolution", "1920x1080");

        // 整数参数:帧率(默认为30fps)

        this->declare_parameter<int>("fps", 30);

        // 布尔参数:自动曝光(默认开启)

        this->declare_parameter<bool>("auto_exposure", true);

        // 浮点参数:曝光时间(默认为100.0ms,仅auto_exposure=false时生效)

        this->declare_parameter<double>("exposure_time", 100.0);

        // 打印初始化日志

        RCLCPP_INFO(this->get_logger(),"参数节点已启动,初始参数如下:");

        this->print_all_params();

        // 创建定时器(每3秒打印一次参数并检测动态修改)

        this->timer_ = this->create_wall_timer(

            std::chrono::seconds(3),                                 // 定时周期3秒

            std::bind(&CameraParamNode::print_all_params, this) // 绑定打印函数

        );

    }

private:

    // 打印所有参数的函数

    void print_all_params()

    {

        // 获取参数值

        std::string resolution;

        this->get_parameter("resolution", resolution); // 方式1:将参数值获取后存入变量

        int fps = this->get_parameter("fps").as_int(); // 方式2:通过Parameter对象获取并返回

        bool auto_exposure = this->get_parameter("auto_exposure").as_bool();

        double exposure_time = this->get_parameter("exposure_time").as_double();

        // 打印参数并格式化输出

RCLCPP_INFO(this->get_logger(),"=====================================");

        RCLCPP_INFO(this->get_logger(),"当前参数:");

        RCLCPP_INFO(this->get_logger(),"1. 分辨率:%s", resolution.c_str());

        RCLCPP_INFO(this->get_logger(),"2. 帧率:%d fps", fps);

        RCLCPP_INFO(this->get_logger(),"3. 自动曝光:%s", auto_exposure ? "开启" : "关闭");

        RCLCPP_INFO(this->get_logger(),"4. 曝光时间:%.1f ms", exposure_time);

 RCLCPP_INFO(this->get_logger(),"=====================================");

    }

    // 定时器对象(类成员变量,用于定时打印)

    rclcpp::TimerBase::SharedPtr timer_;

};

// 主函数(程序入口)

int main(int argc, char *argv[])

{

    // 初始化ROS 2

    rclcpp::init(argc, argv);

    // 创建节点实例

    auto node = std::make_shared<CameraParamNode>();

    // 自旋节点(持续运行,处理定时器回调)

    rclcpp::spin(node);

    // 关闭ROS 2

    rclcpp::shutdown();

    return 0;

}

在上面代码中,我们创建了一个相机参数节点类CameraParamNode,在该类的构造函数中使用函数declare_parameter声明了4个参数,分别为分辨率resolution、帧率fps、自动曝光auto_exposure以及曝光时间exposure_time。创建定时器timer_绑定打印函数print_all_params,并在该函数中使用ROS 2参数API get_parameter获取相机参数,实现每三秒打印输出一次。

备注:父类构造函数的第一个参数camera_param_node即为该节点在ROS 2系统中的节点名称。代码段this->get_parameter("auto_exposure").as_bool()中的.as_bool()的作用是将获取到的参数转换为bool类型再赋值给指定的变量。

节点代码编写完成后,打开配置文件CMakeLists.txt添加相机参数节点的编译、安装规则,配置代码如下所示:

# 添加相机参数节点可执行文件

add_executable(param_demo src/param_demo.cpp)

# 链接依赖库

ament_target_dependencies(

  param_demo

  rclcpp  # 仅需rclcpp,不需要4.2.1节中的SRV接口

)

…

#安装可执行文件,将编译后的节点放到install目录,能够被ros2 run运行

install(TARGETS

    …

    param_demo  # 新增相机参数节点

    # 安装路径:install/service_paramadd_demo/lib/service_paramadd_demo

    DESTINATION lib/${PROJECT_NAME}

)

配置完成后,终端进入工作空间chapter4_ws后,使用构建工具colcon对相机参数节点进行构建编译,并更新终端环境变量,再使用命令ros2 run运行构建编译好的节点,命令如下所示:

colcon build --packages-select service_paramadd_demo

source install/setup.bash

ros2 run service_paramadd_demo param_demo

构建编译完成后,运行相机参数节点,将会在终端中每隔三秒输出一次节点参数信息,如图4.6所示。

图4.6  运行相机参数节点后输出其参数信息

可以看到,运行相机参数节点后,将在终端中每隔三秒输出一次相机节点的参数。这里显示的参数只是声明时的默认值。我们可以使用命令ros2 param查看节点参数、获取指定参数命令,动态修改参数操作。具体操作如下所示:

(1)查看名称为camera_param_node的相机节点中的所有参数以及输出节点指定参数值,命令如下所示:

source install/setup.bash                      #更新环境变量

ros2 param list /camera_param_node             #查看节点的所有参数

ros2 param get /camera_param_node resolution   #输出节点当前的分辨率

以上两条命令将分别输出节点的当前所有参数信息和节点指定参数值,如图4.7所示。

图4.7  输出相机节点全部参数与指定参数

(2)如果我们需要动态修改相机节点的参数值,可以保持节点终端运行并使用组合键Ctrl+Shift+5打开一个新终端,更新环境变量后,同样使用命令ros2 param修改参数,并且可以在原节点运行终端中查看动态修改参数后的效果。命令如下所示:

#修改分辨率为1280×720

ros2 param set /camera_param_node resolution "1280x720"

#修改帧率为60fps

ros2 param set /camera_param_node fps 60

#关闭自动曝光

ros2 param set /camera_param_node auto_exposure false

#修改曝光时间为200.0ms

ros2 param set /camera_param_node exposure_time 200.0

每次成功修改参数后,终端中都会输出提示信息Set parameter successful,并且在相机参数节点的终端中,约3秒后也会显示更新后的参数信息,如图4.8所示。

图4.8  动态修改参数并通报更新显示

我们修改参数后,不需要重启节点即可完成参数的动态更新操作。但是参数的动态修改只针对当前节点运行时有效,当我们重启节点后,其修改的参数也会随之失效。若我们希望节点重启后仍然可以继续使用修改后的参数,那么可以将修改后的当前节点参数保存到.yaml文件中,当再次启动节点时,将该文件作为节点运行的参数即可。

在工作空间chapter4_ws下创建一个目录ros2_yaml用于存放.yaml文件,命令如下所示:

mkdir -p ros2_yaml                         #创建.yaml文件存放目录

ros2 param dump /camera_param_node --output-dir ros2_yaml     #保存文件

命令运行后,将在工作空间下创建一个目录ros2_yaml,我们在执行命令ros2 param前,必须首先运行节点camera_param_node并执行参数修改操作。否则,终端将会提示找不到节点或者保存的参数为节点的默认参数,如图4.9所示。

如图4.9所示,我们将节点名称为camera_param_node的当前所有参数导出到指定目录ros2_yaml中,并创建一个默认以节点名命名的.yaml文件,即camera_param_node.yaml。完成.yaml文件创建后,我们可以使用组合键Ctrl+C在终端中停止运行节点,然后再次运行该节点,并通过参数--ros-args加载.yaml文件,以实现节点重启后仍然可以使用上一次关闭前修改的参数,命令如下所示:

ros2 run service_paramadd_demo param_demo --ros-args --params-file ros2_params/camera_param_node.yaml

图4.9  执行文件保存命令

为了使读者直观看到读取.yaml文件的节点运行参数为上次关闭节点前所修改的参数值,这里使用了一个终端不读取.yaml文件而直接运行的节点作为对比,其参数仍然是默认值,如图4.10所示。

图4.10  读取.yaml文件运行节点的比较效果

备注:如图4.10所示,用户进行实际项目开发时,可以将修改的参数在节点停止运行前保存在.yaml文件中,便于下次再启动该节点时,能够直接运行用户参数。

本小节主要介绍了ROS 2参数通信C++ 实现所需的核心API函数及其使用方法,并通过实例讲解了参数通信的原理、动态修改参数以及参数保存等知识点。通过本小节的学习,读者应能够熟练运用API函数编写ROS 2参数通信程序,同时掌握参数的动态修改技巧以及通过.yaml文件实现参数固化的方法。

4.2.3  Python实现参数操作

在4.2.2节中,我们使用C++实现了ROS 2参数操作示例。而Python实现参数操作的API与C++类似,但我们使用Python API不需要再手动指定类型,使用更加直观,语法也更加简洁。本小节将使用Python及其API函数实现相机节点参数操作,重点介绍Python开发ROS 2参数通信的相关API函数及其使用。

1. 核心API

我们开发ROS 2参数通信时,在Python类rclpy.node.Node中同样提供了常用的参数通信操作API函数,其功能及使用示例如表4.6所示。

表4.6  Python开发ROS 2参数通信的API函数

API函数

功    能

以分辨率参数为示例

declare_parameter(name,default_value)

声明参数为自动推导类型

self.declare_parameter("resolution", "1920x1080")

get_parameter(name)

获取参数对象,包含参数名、参数值、参数类型

param = self.get_parameter("resolution")

get_parameter(name).属性

直接获取参数值,不需要转换对象

res=self.get_parameter("resolution").value

set_parameters([Parameter(name, value)])

设置参数值,直接传Parameter列表

self.set_parameters([Parameter("resolution", "1280x720")])

delete_parameter(name)

删除指定名称的参数

self.delete_parameter("resolution")

has_parameter(name)

检查参数是否存在

if self.has_parameter("resolution"): ...

如表4.6所示,我们通过Python API函数get_parameter可以直接获取节点参数中对应数据类型的值。例如,表中示例的res = self.get_parameter("resolution").value表示将参数中的字符串类型的值返回并赋给变量res,而不需要像C++那样再调用as_bool()等进行类型转换。

2. Python实现参数操作

在功能包service_paramadd_demo下的目录scripts中创建一个Python代码文件param_demo.py并写入代码,如下所示:

#!/usr/bin/env python3

# 导入必要的模块

import rclpy

from rclpy.node import Node

from rclpy.parameter import Parameter  # 参数类

# 定义参数节点类

class CameraParamNodePy(Node):

    def __init__(self):

        # 初始化父类Node(节点名称:camera_param_node_py)

        super().__init__("camera_param_node_py")

        # 声明参数,自动推导类型

        # 分辨率(string类型)

        self.declare_parameter("resolution", "1920x1080")

        # 帧率(int类型)

        self.declare_parameter("fps", 30)

        # 自动曝光(bool类型)

        self.declare_parameter("auto_exposure", True)

        # 曝光时间(double类型)

        self.declare_parameter("exposure_time", 100.0)

        # 打印初始化日志

        self.get_logger().info("Python相机参数节点已启动,初始参数如下:")

        self.print_all_params()

        # 创建定时器(每3秒输出打印一次参数)

        self.timer = self.create_timer(

            3.0,  # 周期3秒

            self.print_all_params  # 绑定打印函数,不需要使用std::bind

        )

    # 打印所有参数的函数

    def print_all_params(self):

        # 直接获取参数值,不需要像C++一样进行转换

        resolution = self.get_parameter("resolution").value

        fps = self.get_parameter("fps").value

        auto_exposure = self.get_parameter("auto_exposure").value

        exposure_time = self.get_parameter("exposure_time").value

        # 输出打印参数

        self.get_logger().info("=====================================")

        self.get_logger().info("当前节点参数:")

        self.get_logger().info(f"1. 分辨率:{resolution}")

        self.get_logger().info(f"2. 帧率:{fps} fps")

        self.get_logger().info(f"3. 自动曝光:{'开启' if auto_exposure else '关闭'}")

        self.get_logger().info(f"4. 曝光时间:{exposure_time:.1f} ms")

        self.get_logger().info("=====================================")

# 主函数

def main(args=None):

    # 初始化ROS 2

    rclpy.init(args=args)

    # 创建节点实例

    node = CameraParamNodePy()

    # 阻塞式运行

    rclpy.spin(node)

    # 关闭节点

    node.destroy_node()

    rclpy.shutdown()

if __name__ == '__main__':

    main()

在上面代码中,第一行代码#!/usr/bin/env python3表示使用Python解释器的版本,后面不能有任何注释或多余的字符。然后创建了参数节点类CameraParamNodePy及名称为camera_param_node_py的节点,并声明了4个参数,分别为分辨率、帧率、自动曝光以及曝光时间。再通过创建定时器并每三秒调用一次回调函数print_all_params实现参数值的输出打印。

上述代码编写完成后,需要为Python代码文件param_demo.py添加可执行权限,命令如下所示:

chmod +x src/service_paramadd_demo/service_paramadd_demo/param_demo.py

可执行权限添加完成后,在配置文件CMakeLists.txt中的字段install下添加Python代码文件param_demo.py的安装路径,如下所示:

…

#安装Python脚本(scripts目录下的可执行文件)

install(PROGRAMS

  scripts/add_server.py

  scripts/add_client.py

  scripts/param_demo.py

  DESTINATION lib/${PROJECT_NAME}

)

…

配置完成后,在终端中进入工作空间下,使用构建工具colcon进行构建,然后运行Python节点,命令如下所示:

colcon build --packages-select service_paramadd_demo

source install/setup.bash

ros2 run service_paramadd_demo param_demo.py

节点运行后,将会在终端中每隔三秒输出一次节点默认参数,如图4.11所示。

图4.11  Python节点输出参数默认值

接下来,我们可以通过动态修改参数实现节点不重启即可更新参数值。使用组合键Ctrl+Shift+5新建一个终端,并输入如下命令:

source install/setup.bash

# 修改分辨率为800×600

ros2 param set /camera_param_node_py resolution "800x600"

# 修改帧率为20fps

ros2 param set /camera_param_node_py fps 20

与C++动态修改参数一样,Python节点运行终端中会打印更新后的参数,如图4.12所示。

图4.12  Python节点参数动态修改后更新显示

可以看到,使用Python编写ROS 2参数通信程序与C++开发相比,更加方便、快速,不需要复杂的配置。因此,Python适用于快速调试ROS 2的参数逻辑。

备注:动态修改节点参数时,必须保持节点处于运行状态,否则修改操作会提示找不到节点。当节点停止后再次启动,参数将恢复为默认值。如果需要持久化保存修改后的参数,可以按照C++保持修改参数的方法,使用.yaml文件进行参数保存与加载。

本节主要讲解了ROS 2参数通信的原理与实现方法,并通过C++和Python分别演示了参数操作节点的程序编写、配置以及参数的动态修改等知识点。通过本节的学习,读者应能够深入理解参数通信的原理,并能够参照示例独立实现ROS 2参数通信程序的功能扩展。

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐