ros学习记录(3)——ROS通信机制之服务通信与参数服务器

通信机制学习过程记录,ROS核心部分

Posted by R on 2023-05-10

前言

接上一章,对ros的服务通信机制进行学习,服务通信和话题通信是ros通信机制的两大块,熟练掌握这两块内容才能更好的使用ros。后半部分对参数服务器进行了介绍,参数服务器作为数据存储的公共容器,同样具有着不可替代的重要性。关于流程指令和各个模块常用命令的梳理,之后会有一章进行比较详细的介绍。

服务通信

服务通信也是ROS中一种极其常用的通信模式,服务通信是基于请求响应模式的,是一种应答机制。也即: 一个节点A向另一个节点B发送请求,B接收处理请求并产生响应结果返回给A。比如如下场景:

机械臂在待机状态中,控制系统分析视觉传感器数据发现需要夹取的工件,此时需要获取图像进行坐标获取。

上述场景中就使用到了服务通信。一个节点需要向相机节点发送拍照请求,相机节点处理请求,并返回处理结果。

与上述应用类似的,服务通信更适用于对实时性有要求、具有一定逻辑处理的应用场景。比如说在本教程中,要实现两个数字的求和,客户端节点运行会向服务器发送两个数字,服务器端节点接收两个数字求和并将结果响应回客户端。

服务通信理论模型

服务通信较之于话题通信更简单些,理论模型如下图所示,该模型中涉及到三个角色:ROS master(管理者)、Server(服务端)、Client(客户端)

ROS Master 负责保管 Server 和 Client 注册的信息,并匹配话题相同的 Server 与 Client ,帮助 Server 与 Client 建立连接,连接建立后,Client 发送请求信息,Server 返回响应信息。

实现流程如下:

  • Server注册
    Server 启动后,会通过RPC在 ROS Master 中注册自身信息,其中包含提供的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。

  • Client注册
    Client 启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要请求的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。

  • ROS Master实现信息匹配
    ROS Master 会根据注册表中的信息匹配Server和 Client,并通过 RPC 向 Client 发送 Server 的 TCP 地址信息。

  • Client发送请求
    Client 根据步骤2 响应的信息,使用 TCP 与 Server 建立网络连接,并发送请求数据。

  • Server发送响应
    Server 接收、解析请求的数据,并产生响应结果返回给 Client。

注意:客户端请求被处理时,需要保证服务器已经启动

服务通信自定义srv

需求:服务通信中,客户端提交两个整数至服务端,服务端求和并响应结果到客户端,请创建服务器与客户端通信的数据载体。
流程:srv 文件内的可用数据类型与 msg 文件一致,且定义 srv 实现流程与自定义 msg 实现流程类似:按照固定格式创建srv文件——编辑配置文件——编译生成中间文件。

  1. 定义srv文件

    服务通信中,数据分成两部分,请求与响应,在 srv 文件中请求和响应使用—-分割,具体实现如下:

    功能包下新建 srv 目录,添加 xxx.srv 文件,内容:

    1
    2
    3
    4
    5
    6
    # 客户端请求时发送的两个数字
    int32 num1
    int32 num2
    ---
    # 服务器响应发送的数据
    int32 sum
  2. 编辑配置文件

    package.xml中添加编译依赖与执行依赖

     
    1
    2
    3
    4
    5
    <build_depend>message_generation</build_depend>
    <exec_depend>message_runtime</exec_depend>
    <!--
    exce_depend 以前对应的是 run_depend 现在非法
    -->

    CMakeLists.txt编辑 srv 相关配置

     
    1
    2
    3
    4
    5
    6
    7
    find_package(catkin REQUIRED COMPONENTS
    roscpp
    rospy
    std_msgs
    message_generation
    )
    # 需要加入 message_generation,必须有 std_msgs
    1
    2
    3
    4
    add_service_files(
    FILES
    AddInts.srv
    )
    1
    2
    3
    4
    generate_messages(
    DEPENDENCIES
    std_msgs
    )

    注意: 官网没有在 catkin_package 中配置 message_runtime,经测试配置也可以

  3. 编译

    Python 需要调用的中间文件(…/工作空间/devel/lib/python3/dist-packages/包名/srv)
    1

服务通信自定义srv调用(Python)

需求:编写服务通信,客户端提交两个整数至服务端,服务端求和并响应结果到客户端。
分析:在模型实现中,ROS master 不需要实现,而连接的建立也已经被封装了,需要关注的关键点有三个:服务端、客户端、数据。
流程:编写服务端实现——编写客户端实现——为python文件添加可执行权限——编辑配置文件——编译并执行

  1. vscode配置

    需要像之前自定义 msg 实现一样配置settings.json 文件,如果以前已经配置且没有变更工作空间,可以忽略,如果需要配置,配置方式与之前相同:

     
    1
    2
    3
    4
    5
    {
    "python.autoComplete.extraPaths": [
    "/opt/ros/noetic/lib/python3/dist-packages",
    ]
    }
  2. 服务端

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    #! /usr/bin/env python
    """
    需求:
    编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器
    服务器需要解析客户端提交的数据,相加后,将结果响应回客户端,
    客户端再解析

    服务器端实现:
    1.导包
    2.初始化 ROS 节点
    3.创建服务对象
    4.回调函数处理请求并产生响应
    5.spin 函数

    """
    # 1.导包
    import rospy
    from demo03_server_client.srv import AddInts,AddIntsRequest,AddIntsResponse
    # 回调函数的参数是请求对象,返回值是响应对象
    def doReq(req):
    # 解析提交的数据
    sum = req.num1 + req.num2
    rospy.loginfo("提交的数据:num1 = %d, num2 = %d, sum = %d",req.num1, req.num2, sum)

    # 创建响应对象,赋值并返回
    # resp = AddIntsResponse()
    # resp.sum = sum
    resp = AddIntsResponse(sum)
    return resp


    if __name__ == "__main__":
    # 2.初始化 ROS 节点
    rospy.init_node("addints_server_p")
    # 3.创建服务对象
    server = rospy.Service("AddInts",AddInts,doReq)
    # 4.回调函数处理请求并产生响应
    # 5.spin 函数
    rospy.spin()
  3. 客户端:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    #! /usr/bin/env python

    """
    需求:
    编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器
    服务器需要解析客户端提交的数据,相加后,将结果响应回客户端,
    客户端再解析

    客户端实现:
    1.导包
    2.初始化 ROS 节点
    3.创建请求对象
    4.发送请求
    5.接收并处理响应

    优化:
    加入数据的动态获取


    """
    #1.导包
    import rospy
    from demo03_server_client.srv import *
    import sys

    if __name__ == "__main__":

    #优化实现
    if len(sys.argv) != 3:
    rospy.logerr("请正确提交参数")
    sys.exit(1)


    # 2.初始化 ROS 节点
    rospy.init_node("AddInts_Client_p")
    # 3.创建请求对象
    client = rospy.ServiceProxy("AddInts",AddInts)
    # 请求前,等待服务已经就绪
    # 方式1:
    # rospy.wait_for_service("AddInts")
    # 方式2
    client.wait_for_service()
    # 4.发送请求,接收并处理响应
    # 方式1
    # resp = client(3,4)
    # 方式2
    # resp = client(AddIntsRequest(1,5))
    # 方式3
    req = AddIntsRequest()
    # req.num1 = 100
    # req.num2 = 200

    #优化
    req.num1 = int(sys.argv[1])
    req.num2 = int(sys.argv[2])

    resp = client.call(req)
    rospy.loginfo("响应结果:%d",resp.sum)
  4. 设置权限:

    终端下进入 scripts 执行: chmod +x xxx.py

  5. 配置 CMakeLists.txt:

    1
    2
    3
    4
    5
    catkin_install_python(PROGRAMS
    scripts/AddInts_Server_p.py
    scripts/AddInts_Client_p.py
    DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
    )
  6. 执行

    • 流程:
        先启动服务:rosrun 包名 服务
        然后再调用客户端: rosrun 包名 客户端 参数1 参数2
      

参数服务器

参数服务器在ROS中主要用于实现不同节点之间的数据共享。参数服务器相当于是独立于所有节点的一个公共容器,可以将数据存储在该容器中,被不同的节点调用,当然不同的节点也可以往其中存储数据,关于参数服务器的典型应用场景如下:

机械臂导航实现时,会进行路径规划,比如: 全局路径规划,设计一个从出发点到目标点的大致路径。本地路径规划,会根据当前路况生成时时的行进路径

上述场景中,全局路径规划和本地路径规划时,就会使用到参数服务器:路径规划时,需要参考机械臂的尺寸,我们可以将这些尺寸信息存储到参数服务器,全局路径规划节点与本地路径规划节点都可以从参数服务器中调用这些参数,因此参数服务器,一般适用于存在数据共享的一些应用场景。(类似于全局变量)

参数服务器理论模型

参数服务器实现是最为简单的,该模型如下图所示,该模型中涉及到三个角色:ROS Master (管理者)、Talker (参数设置者)、Listener (参数调用者)

ROS Master 作为一个公共容器保存参数,Talker 可以向容器中设置参数,Listener 可以获取参数。
1
整个流程由以下步骤实现:

  1. Talker 设置参数
    Talker 通过 RPC 向参数服务器发送参数(包括参数名与参数值),ROS Master 将参数保存到参数列表中。

  2. Listener 获取参数
    Listener 通过 RPC 向参数服务器发送参数查找请求,请求中包含要查找的参数名。

  3. ROS Master 向 Listener 发送参数值
    ROS Master 根据步骤2请求提供的参数名查找参数值,并将查询结果通过 RPC 发送给 Listener。

注意:参数服务器不是为高性能而设计的,因此最好用于存储静态的非二进制的简单数据

参数操作(Python)

需求:实现参数服务器参数的增删改查操作

  1. 参数服务器新增(修改)参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #! /usr/bin/env python
    """
    参数服务器操作之新增与修改(二者API一样)_Python实现:
    """

    import rospy

    if __name__ == "__main__":
    rospy.init_node("set_update_paramter_p")

    # 设置各种类型参数
    rospy.set_param("p_int",10)
    rospy.set_param("p_double",3.14)
    rospy.set_param("p_bool",True)
    rospy.set_param("p_string","hello python")
    rospy.set_param("p_list",["hello","haha","xixi"])
    rospy.set_param("p_dict",{"name":"hulu","age":8})

    # 修改
    rospy.set_param("p_int",100)
  2. 参数服务器获取参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    #! /usr/bin/env python

    """
    参数服务器操作之查询_Python实现:
    get_param(键,默认值)
    当键存在时,返回对应的值,如果不存在返回默认值
    get_param_cached
    get_param_names
    has_param
    search_param
    """

    import rospy

    if __name__ == "__main__":
    rospy.init_node("get_param_p")

    #获取参数
    int_value = rospy.get_param("p_int",10000)
    double_value = rospy.get_param("p_double")
    bool_value = rospy.get_param("p_bool")
    string_value = rospy.get_param("p_string")
    p_list = rospy.get_param("p_list")
    p_dict = rospy.get_param("p_dict")

    rospy.loginfo("获取的数据:%d,%.2f,%d,%s",
    int_value,
    double_value,
    bool_value,
    string_value)
    for ele in p_list:
    rospy.loginfo("ele = %s", ele)

    rospy.loginfo("name = %s, age = %d",p_dict["name"],p_dict["age"])

    # get_param_cached
    int_cached = rospy.get_param_cached("p_int")
    rospy.loginfo("缓存数据:%d",int_cached)

    # get_param_names
    names = rospy.get_param_names()
    for name in names:
    rospy.loginfo("name = %s",name)

    rospy.loginfo("-"*80)

    # has_param
    flag = rospy.has_param("p_int")
    rospy.loginfo("包含p_int吗?%d",flag)

    # search_param
    key = rospy.search_param("p_int")
    rospy.loginfo("搜索的键 = %s",key)
  3. 参数服务器删除参数

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #! /usr/bin/env python
    """
    参数服务器操作之删除_Python实现:
    rospy.delete_param("键")
    键存在时,可以删除成功,键不存在时,会抛出异常
    """
    import rospy

    if __name__ == "__main__":
    rospy.init_node("delete_param_p")

    try:
    rospy.delete_param("p_int")
    except Exception as e:
    rospy.loginfo("删除失败")

这里只给程序是因为流程和前面一样,而且不像话题、服务通信一样有中间文件,只需要修改cmakelists即可,建议自己完成这一部分。如果报错问题无法解决可以参考视频解决

小结

在调试过程中注意自己命名的文件修改问题,另外每个项目的测试可以多定义功能包,也可以在原先的功能包中添加,多做尝试。