• 《机器人操作系统(ROS)浅析》肖军浩译


    PDF下载地址:

    链接:https://pan.baidu.com/s/1l_FAbn_g04q-Z0lKSxx8tw
    提取码:8lbp

    注:英文原文为《A Gentle Introduction to ROS》 【美】Jason M. O'Kane著

    一、绪论

    二、入门概述

    1. 安装:参考 http://wiki.ros.org/cn

    (1)sudo rosdep init  初始化rosdep,rosdep用于检查和安装软件包的依赖

    (2)rosdep update  初始化rosdep,在根目录下保存一些文件,文件夹名为.ros

    (3)source /opt/ros/ros版本/setup.bash

    • 设置ROS_PACKAGE_PATH等环境变量,ROS根据这些环境变量来定位文件  // 输入 export | grep ROS 判断是否配置完成
    • setup.bash还定义了一些ros系统的bash函数,如roscd和rosls  //这些函数定义在rosbash软件包

    (3)测试ROS是否正确安装

    • roscore
    • rosrun turtlesim turtlesim_node
    • rosrun turtlesim turtle_teleop_key

    注:三个终端分别执行指令

    2. 常用术语和命令

    (1)ROS功能包/软件包:一组用于实现特定功能的相关文件的集合,包括可执行文件和其他支持文件  //任何能找到且包含package.xml文件的目录

    注:使用catkin编译生成的可执行文件放在外部标准目录devel中(源码外编译

    (2)节点node:ROS程序的运行实例 running instance

    (3)rosrun命令:启动ROS节点  rosrun  功能包名  可执行文件名  //节点名不一定与可执行文件名相同,可以使用 __name:=节点名 显式设置节点名称

    • rosrun本质上是一个shell脚本,可以根据ROS的文件组织结构自动找到相应可执行文件  //可以直接输入可执行文件的路径启动节点(等价的普通程序执行方式
    • 通过节点管理器master注册成为ROS节点发生在程序的源码内部,而不是通过rosrun命令

    注:节点名称必须唯一,不可多个节点拥有相同名称

    (4)消息传递机制:节点管理器负责确保发布节点和订阅节点能找到对方,随后消息从发布节点直接传递到订阅节点,不经过节点管理器转交

    注:节点只管发,不管收,有助于减少节点之间的耦合度;话题可共享,话题和消息的通信机制是多对多的;服务是一对一的通信机制

    (5)rosmsg show 消息类型名  //输出消息类型的详细信息

    (6)rostopic pub 话题名 消息类型 消息内容  //命令行手动发布消息

    注:消息内容可以按照rosmsg show命令的显示结果进行输入,也可以以YAML字典的形式输入显式指明结构域中变量名和值的映射关系)

    (7)每个消息类型均属于一个特定的包,如turtlesim/Color属于turtlesim包  //消息类型斜杠/前的是包名,斜杠/后是类型名

    注:把包名包含在消息类型中的目的

    • 避免命名冲突  //  geometry_msgs/Pose 和 turtlesim/Pose
    • 在用到其他包的消息类型时,更容易表明包之间的依赖关系
    • 有助于理解消息类型的含义

    三、编写ROS程序

    1. catkin编译系统试图一次性编译同一个工作区中的所有功能包

    2. 创建功能包:catkin_create_pkg 包名  //包名只允许使用小写字母、数字和下划线,且首字符必须为字母

    3. 清单文件package.xml中声明的依赖库并不会在编译过程中进行检查,但是会在将包发布给他人时,他人会因为没有安装依赖库而编译报错

    4. CMakeLists.txt中的${catkin_LIBRARIES}变量由find_package(catkin REQUIRED COMPONENTS ...)语句定义

    5. source devel/setup.bash  //专门为当前的工作区进行环境变量设置

    6. 每个话题都有一个消息类型,而每个消息类型都有相应的C++头文件  //包含消息类型头文件  #include<包名/类型名.h>  如#include<geometry_msgs/Twist.h>

    注:头文件定义了一个与消息类型对应的C++类,且该类定义在以包名命名的域名空间中,如geometry_msgs::Twist类

    7. ros::NodeHandle类维护一个引用计数,仅在第一个NodeHandle对象创建时才会在节点管理器注册新的节点  //使用标准的roscpp接口,在单个程序中无法运行多个节点

    8. 创建发布者ros::Publisher属于耗时操作,应该每个话题创建一个发布者,在程序执行过程中一直使用相应发布者  //发布者创建不要放在循环内,避免为每条消息生成新的发布者

    9. ros::ok()返回false条件:

    • 使用rosnode kill命令终止节点
    • 使用Ctrl+C发送终止信号  //需要采用rosnode cleanup从节点管理器的列表中删除节点
    • 调用ros::shutdown()  //在代码中显式表明节点工作已完成
    • 以相同节点名启动新的节点  //新的节点将正常运行,而旧节点会被终止

    10. 订阅者回调函数:void function_name(const package_name::type_name &msg){ ... }

    • 调用回调函数是ROS执行,返回值也交给ROS,用户程序无法获得返回值,因此设置为void
    • 回调函数指针为 &function_name,如程序中的 &poseMessageReceived  // 取地址符&是可选的,但建议使用,编译器会根据函数名后面是否有括号,自动判断是指针还是函数调用 

    11. 显式调用 ros::spin()或ros::spinonce()让ROS执行回调函数

    四、日志消息  //ROS使用log4cxx库实现日志功能

    1. ROS日志系统的核心思想:使程序生成一些简短的文本字符流,即日志消息

    2. 严重级别(递增):DEBUG、INFO、WARN、ERROR、FATAL

    3. 产生日志消息的基本C++宏

    • ROS_DEBUG_STREAM(message)
    • ROS_INFO_STREAM(message)
    • ROS_WARN_STREAM(message)
    • ROS_ERROR_STREAM(message)
    • ROS_FATAL_STREAM(message)

    注:日志系统是面向行的,调用任意宏会生成完整的一行日志消息  //无需使用std::endl

    4. 日志消息的输出  

    (1)控制台

    • DEBUG和INFO消息被送至标准输出,WARN、ERROR和FATAL消息被送至标准错误
    • 设置ROSCONSOLE_FORMAT环境变量调整日志消息打印到控制台的格式
    • roslaunch工具默认不会将节点的标准输出和标准错误导入至自己的输入流,需显式使用 output="screen"属性或者--screen参数

    (2)/rosout 话题:消息类型为rosgraph_msgs/Log

    • 包含系统中所有节点的日志消息
    • 查看消息内容:rostopic echo /rosout 或者 rqt_console

    • rqt_console节点订阅/rosout_agg话题  //后缀_agg表示消息是经过rosout节点聚合的结果
    • /rosout话题发布的消息都通过rosout节点输出到/rosout_agg话题
    • rosout是唯一一个订阅/rosout话题的节点,也是/rosout_agg话题唯一的发布者  //减少调试代价

    注:rosout即表示话题,也表示节点

    (3)日志文件:作为/rosout话题回调函数的一部分,由rosout节点生成

    • 文件名类似于:~/.ros/log/run_id/rosout.log  //纯文本文件,用less、head、tail等命令查看
    • 查看运行标识码run_id  //节点管理器开始运行时基于MAC地址和当前时间生成
    1. 查看roscore的输出结果:setting /run_id to run_id
    2. 向节点管理器查询:rosparam get /run_id  //run_id存放在参数服务器中

    检查日志文件大小:rosclean check

    删除日志文件:rosclean purge

    5. 设置日志级别

    (1)通过命令行设置:rosservice call /node-name /set_logger_level ros.包名 level  // level参数包括DEBUG、INFO、WARN、ERROR、FATAL

    注:参数ros.包名指定期望配置的日志记录器logger名称

    (2)通过图形界面设置:rqt_logger_level

    (3)通过C++代码设置:调用ROS实现日志功能的log4cxx提供的接口  #include<log4cxx/logger.h>

    五、计算图源命名

    1. 计算图源graph resource:节点、话题、服务和参数的统称,由短字符串表示的计算图源名称进行标识

    2. 全局名称:/turtle1/cmd_vel  //优点:任何地方都可以使用;缺点:需要完整列出其所属的命名空间

    • 由前斜杠"/"作为首字符
    • 由斜杠分开一系列命名空间,每个斜杠代表一级命名空间,如turtle1命名空间
    • 命名空间用于将相关的计算图源归类在一起,ROS允许多层命名空间嵌套
    • 最末为基本名称base name,如cmd_vel

    3. 相对名称  //利用ROS提供的默认命名空间

    (1)典型特征:缺少全局名称带有的前斜杠"/"

    (2)解析相对名称:将当前的默认命名空间名称加在相对名称之前,生成全局名称,如 /turtle1 + cmd_vel => /turtle1/cmd_vel

    (3)设置默认命名空间:单独为每个节点设置  //不设置,则使用全局命名空间"/"作为默认命名空间

    • 在启动文件中使用命名空间ns属性
    • 利用命令行参数 __ns:=default-namespace
    • 利用环境变量设置shell:export ROS_NAMESPACE=default-namespace  //仅当未设置__ns参数时才有效

    优点:避免在移植和整合来自不同来源的节点时发生名称冲突

    4. 私有名称  //用于节点内部仅与本节点有关的资源,如参数

    (1)典型特征:以一个波浪字符(~)开始

    (2)与相对名称一致,不能完全确定自身所在命名空间,需要利用ROS客户端库进行名称解析

    (3)不使用当前的默认命名空间,而采用节点名称作为命名空间,如  /sim1/pubvel + ~max_vel => /sim1/pubvel/max_vel

    (4)私有名称可用于管理节点的服务,话题不能命名为私有名称

    注:私有名称的关键字"private"仅表明不使用所在的命名空间,其他节点可以通过私有名称解析后的全局名称进行访问  //仅在命名空间层面上有意义,注意与其他编程语言的不同

    5. 匿名名称:非用户指定的无语义信息的名字

    (1)一般用于为节点命名,使节点的命名保存唯一性

    (2)调用ros::init方法时请求一个自动分配的唯一名称

    • ros::init(argc, argv, base_name, ros::init_options::AnonymousName);

    注:ros::init使用处理器时间在节点的基本名称后追加文本,保证名字的唯一性

    六、启动文件

    1. 目的:利用启动文件一次性配置和运行多个节点

    2. 执行启动文件:roslaunch 包名 启动文件名  //如果没有运行roscore,roslaunch会自动启动roscore

    (1)如果启动文件不属于任何功能包,则可以直接以启动文件路径启动 roslaunch + 启动文件路径

    (2)启动文件内的节点几乎是同一时刻启动,无法确定启动顺序,因此节点之间应尽量保持独立

    (3)添加-v选项可以输出详细信息:roslaunch -v 包名 启动文件名

    (4)查找启动文件时,roslaunch工具会同时搜索每个功能包目录的子目录

    3. 启动文件基本元素

    (1)根元素:<launch> ... </launch>

    (2)启动节点:

    <node

      pkg=包名

      type=可执行文件名

      name=节点名

    />

    • 标签末尾的斜杠"/"不可少,另一种方式为 <node pkg="..." type="..." name="..."></node>  //显式给出结束标签
    • 必需属性pkg,type,name(会覆盖ros::init设置的名称
    • 默认情况下,启动文件启动节点的标准输出被重定向至日志文件~/.ros/log/run_id/node_name-number-stout.log
    • 在节点元素中配置属性output="screen"可以在控制台终端输出信息
    • 使用--screen 命令行选项在控制台显式所有节点输出:roslaunch --screen 包名 启动文件名

    (3)请求复位:respawn="true"  //当节点停止时,roslaunch会重新启动该节点,可用于应对软件崩溃、硬件故障等引起的节点中止

    (4)必要节点:required="true"  //当必要节点中止时,roslaunch会终止所有其他活跃节点,并退出;与respawn作用相互矛盾,不能同时配置

    (5)roslaunch所有节点共享一个终端,针对依赖控制台输入的节点来说,需要维护独立的终端:使用启动前缀属性  launch-prefix="命令行前缀"

    • roslaunch启动节点时调用相应的命令行工具rosrun
    • 启动前缀相当于在rosrun前添加前缀,即示例中的launch-prefix="xterm -e" 等价于 xterm -e rosrun turtlesim turtle_teleop_key

    3. 在命名空间内启动节点  //通过配置节点元素的ns属性,压入命名空间

    (1)尽管两个节点相对名称相同,但是由于命名空间不同,所以两个节点相互独立

    (2)由于话题名称定义时采用的也是相对名称,所以话题也分别位于独立的命名空间

    4. 名称重映射remapping names  //从更精细的层面控制节点名称的修改

    (1)基于替换的思想:每个重映射包含一个原始名称和一个新名称,每当节点使用原始名称时,ROS客户端库会将它自动替换为新名称

    (2)创建重映射

    • 命令行启动时,分别给出原始名称和新名称  原始名称:=新名称  //rosrun turtlesim turtlesim_node turtl1/pose:=tim
    • 启动文件中,使用重映射元素:  <remap from="original-name" to "new-name"/>  //若属性作为launch元素的子元素出现在顶层,则应用到所有后续节点

      注:重映射元素也可以作为节点的子元素:<node ...> <remap from="original-name" to "new-name"/> </node> 此时只应用于所在节点

    注:

    • ROS在应用任何重映射之前,所有的名称需要先解析为全局名称
    • 应用示例:反向海龟  //将原始速度话题重映射为处理后的反向速度话题

    5. 其他元素

    (1)启动文件的嵌套:include元素

    • <include file="启动文件完整路径"/>  //通常使用find命令搜索功能包位置,避免路径输入错误,如<include file="$(find package-name)/launch-file-name"/>
    • 支持命名空间属性,可以将包含内容压入指定命名空间:<include file="..." ns="namespace"/>

    (2)启动参数argument:便于配置启动文件,类似可执行程序中的局部变量(仅在声明的当前启动文件内有效,不可被包含的启动文件继承)

    • 声明参数:<arg name="arg-name"/>  //声明是可选的,有助于明确启动文件的参数有哪些
    • 参数赋值
      • 命令行赋值  roslaunch 包名 启动文件名 arg-name:=arg-value
      • 启动文件内声明时赋值
      1. <arg name="arg-name" default="arg-value"/>  //可被命令行参数覆盖
      2. <arg name="arg-name" value="arg-value"/>  //不可被命令行参数覆盖
    • 获取参数值:$(arg arg-name)
    • 向包含的次级启动文件中发送参数值
      • 将arg元素作为include元素的子元素  <include ...> <arg name="arg-name" value="arg-value"/> </include>
      • 可使用<arg name="arg-name" value="$(arg arg-name)"/> 保证内外同名参数具有相同的参数值

    注:在ROS中,parameter和argument是有区别的

    • parameter是ROS系统运行过程中使用的数值,存储在参数服务器parameter server中,可以被节点和用户获取(ros::param::get或rosparam)
    • argument只在启动文件内有意义,不能被节点直接获取

    (3)创建组group:大型启动文件内管理节点的快捷方式

    • 把若干节点放入同一命名空间中:<group ns="namespace"> ... </group>
    • 可以有条件地启动或禁用一个节点:<group if="0 or 1"> ... </group>  //若属性为1,则正常启动,否则忽略组标签内元素;若 if 改为 unless 则用法相反

    注:仅有0和1为合法取值,且不能使用布尔运算;也可以不使用组元素,单独为每个节点设置ns、if和unless

    七、参数parameter  //配置节点信息的集中式方法

    1. 主要思想:使用集中的参数服务器维护一个变量集的值(字典),适用于不会随时间频繁变更的信息

    2. 通过命令行获取参数

    (1)rosparam list  //查看参数列表,输出 /rosdistro全局计算图源的名称字符串

    (2)rosparam get parameter_name  //查询参数,如 rosparam get /rosdistro 得到ROS版本

    (3)rosparam get namespace  //检索给定命名空间每个参数的值,如 rosparam get / 查询全局命名空间/

    (4)rosparam set parameter_name parameter_value  //设置参数,修改已有参数的值或创建新参数

    (5)rosparam set namespace values  //设置同一命名空间的多个参数,其中值values要以YAML字典的形式给出参数和值的对应关系

    (6)创建和加载参数文件(YAML形式)  //可用于场景复现

    • rosparam dump filename namespace  //存储命名空间中的所有参数
    • rosparam load filename namespace  //读取参数至参数服务器

      注:命名空间参数可选,默认为全局命名空间/

    注:

    • 所有的参数都属于参数服务器,而不是任何特定节点,即使节点终止时参数仍然存在
    • 更新的参数值不会自动“推送”到节点,节点需要主动显式地向参数服务器查询参数值

    3. 使用C++接口获取参数  //参数名可以是全局的、相对的或者私有的

    void ros::param::set(parameter_name, input_value);

    bool ros::param::get(parameter_name, output_value);  //读取成功返回true,否则表明参数还未指定值

    注:在命令行中实现私有参数赋值 rosrun agitr pubvel_with_max _max_vel:=1  //参数max_vel前面添加下划线_前缀  _param-name:=param-value

     4. 在启动文件中设置参数

    (1)<param name="param-name" value="param-value"/>  //设置参数,参数名是相对名称

    (2)<node ...> <param name="param-name" value="param-value"/>  </node>  //在节点元素内包含param元素,无论是否以~或/开头,参数均为节点私有参数

    (3)<rosparam command="load" file="path-to-param-file"/>  //一次性从文件中加载多个参数,等价于rosparam load

    注:通常采用 file="$(find package-name)/param-file" 指定文件路径

     八、服务service

    1. 与话题消息的区别

    • 服务调用是双向
    • 服务调用是一对一通信

    2. 执行过程:客户端节点发送请求数据到服务器节点,并且等待回应;服务器节点收到请求后,执行相应活动,随后发送响应数据给客户端节点;

    3. 与话题内容由消息数据类型确定类似,服务的内容也由服务数据类型决定

    4. 从命令行查看和调用服务

    (1)rosservice list  //列出所有服务

    (2)服务一般可以划分为两类:

    • 特定节点获取或向其传递消息  //通常采用节点名作为命名空间以防止命名冲突,如采用私用名称的~get_loggers服务和~set_logger_level服务会被解析为/turtlesim/get_logger和/turtlesim/set_logger_level
    • 不针对某些特定节点的服务  //例如/spawn服务用于生成一个新的仿真海龟,尽管由turtlesim节点提供,但是不属于任何特定节点(只需要完成相应功能,而不关心是哪个节点在起作用

    5. rosnode info node-name  //查看某个节点的服务类型

    6. rosservice node service-name  //查找提供服务的节点(反向查询

    7. rosservice info service-name  //确定服务的数据类型,通常也是由包名和类型名组成“包名/类型名”,如turtlesim/Spawn

    8. rossrv show 服务数据类型名  //获取服务数据类型的详细信息(一系列数据域),注意rosservice和rossrv的区别

    注:

    • 短横线---之前为请求的数据项,之后的字段为响应项
    • 服务的请求和响应字段都可以为空,如turtlesim_node提供的/reset服务的数据类型为std_srvs/Empty,其请求和响应字段均为空

    9. rosservice call service-name request-content  //调用服务,提供请求域的所有值,如rosservice call /spawn 3 3 0 Mikey 在位置(3, 3)处创建一个名为"Mikey"的新海龟,朝向为0

    注:

    • 新海龟有自己的资源集,且位于新命名空间Mikey中  //避免命名冲突
    • rosservice call的输出为服务器节点的响应数据,如上例为 name: Mikey  //调用成功或失败
    • rosservice call /clear 从参数服务器读取参数值,刷新当前参数

     10. 客户端程序

    (1)声明请求和响应类型:#include<turtlesim/Spawn.h>  //包含头文件,定义turtlesim::Spawn类(服务数据类型

    (2)创建客户端对象:ros::ServiceClient client = node_handle.serviceClient<service_type>(service_name)  //服务名称一般应当为相对名称,如"spawn"

    注:与话题发布者不同,无需缓冲队列,会阻塞等待直至服务调用完成

    (3)创建请求和响应对象  //上述头文件中定义了请求和响应的类,通过包名和服务类型引用,如turtlesim::Spawn::

    • package_name::service_type::Request  //请求对象类,应做赋值操作
    • package_name::service_type::Response  //响应对象类,接收服务器响应消息,无需赋值

    注:服务类型的头文件中还定义了一个单独类package_name::service_type同时拥有Request和Response作为数据成员;可以定义单独对象package_name::service_type srv,而不使用独立的Request和Response对象

    (4)调用服务:bool success = service_client.call(request, response)

    • 完成定位服务器节点、传输请求数据、等待响应和存储响应数据等一系列工作
    • 调用方法返回布尔值来确认调用服务是否成功完成

    注:

    • 不要忘记对调用返回值进行判断,可通过ROS_ERROR_STREAM输出可能的错误信息
    • 默认情况下,调用返回后,会关闭与服务器的连接
    • 可通过ros::ServiceClient client = node_handle.serviceClient<service_type>(service_name, true)建立持续连接的客户端  //提高了节点耦合性,系统鲁棒性变差,不建议

    11. 服务器程序

    (1)编写回调函数 bool function_name(package_name::service_type::Request &req, package_name::service_type::Response &resp)

    注:本例中请求和响应的数据类型均为std_srvs/Empty(空字符串),无需进行数据处理

    (2)与话题订阅者类似,需要创建服务器对象ros::ServiceServer server=node_handle.advertiseService(service_name, pointer_to_callback_function);

    • service_name 建议使用局部名称,但不严格限制使用全局名称
    • ros::NodeHandle::advertiseService拒绝接受私有名称,即以波浪号~开头的名称
    • 可以利用节点自身的缺省命名空间创建私有名称服务
    1. ros::NodeHandle nhPrivate("~");  //发送给此节点句柄的任意局部名称的缺省命名空间与节点名称一致 (此句柄+相对名称 效果等价于 私有名称)
    2. ros::ServiceServer server=nhPrivate.advertiseService("baz", Callback);  //与广播名为~baz的服务效果相同

    注:本例使用ros::spinOnce()而非ros::spin(),原因在于没有服务调用时还需要执行其他工作,即发布速度指令  //与第三章 表3.5 进行对比

    (3)解决ros::spinOnce() + sleep()引起的延迟问题

    1. 采用双线程:一个负责发布消息,一个负责处理服务器回调
    2. 采用ros::spin(),并利用计数器回调函数(timer callback)发布消息  // http://wiki.ros.org/roscpp/Overview/Timers

     九、消息录制与回放

    1. rosbag:将发布在一个或多个话题上的消息录制到一个包文件中,可用于事后回放

    注:术语包文件bag files指用于存储带时间戳的ROS消息的特殊文件格式

    2. 命令行工具

    (1)录制包文件:rosbag record -O filename.bag topic-names  //不指定文件名时,rosbag基于当前的日期和时间自动生成文件名;会创建新节点 /record_...

    • rosbag record -a 记录当前发布的所有话题消息
    • rosbag record -j 启动包文件的压缩

    (2)回放包文件:rosbag play filename.bag  //保持与原始发布相同的顺序和时间间隔

    注:尽量避免系统中rosbag和“真实”节点向同一个话题发布消息

    (3)检查文件包:rosbag info filename.bag  //提供包文件的丰富信息

    3. rosbag功能包

    (1)rosrun rosbag record -O filename.bag topic-names;rosrun rosbag play filename.bag  //利用record和play可执行文件

    (2)在启动文件中使用包文件

    • 录制节点:<node pkg="rosbag" name="record" type="record" args='-O filename.bag topic-names"/>
    • 回放节点:<node pkg="rosbag" name="play" type="play" args="filename.bag"/>

    注:需要提供参数

    第十章 总结

    1. 在网络环境中运行ROS:分布式机器人控制模式

    (1)网络层配置:确保计算机之间能够互相通信

    (2)ROS层配置:确保所有节点都能与节点管理器通信

    2. 编写更规范的程序,如使用ros::Timer的回调函数替代ros::Rate对象

    3. 使用rviz是数据可视化:订阅用户话题以显示消息

    4. 创建消息和服务类型

    5. 使用tf工具来管理多个坐标系:帮助节点完成坐标转换

    6. 使用Gazebo仿真:高保真的机器人仿真器

  • 相关阅读:
    【转载】 c++中static的用法详解
    ROS学习 Python读写文本文件
    (论文分析) Machine Learning -- Learning from labeled and unlabeled data
    (论文分析) 图像相似度和图像可见性分析
    (论文分析) Machine Learning -- Support Vector Machine Learning for Interdependent and Structured Output Spaces
    (论文分析) Object Detection-- Discriminatively Trained Part Based Models
    (论文分析) Machine Learning -- Predicting Diverse Subsets Using Structural SVMs
    (论文分析) Machine Learning -- Online Choice of Active Learning Algorithms
    (论文分析) Object Detection -- Class-Specific Hough Forests for Object Detection
    (论文分析) Object Detection -- A Boundary Fragment Model for Object Detection
  • 原文地址:https://www.cnblogs.com/hg-love-dfc/p/10387246.html
Copyright © 2020-2023  润新知