ROS
ROS通信框架
00 分钟
发布于: 2020-4-28
最后更新: 2024-8-18
type
status
date
slug
summary
category
tags
password
Created time
Jul 30, 2024 06:37 PM
Last edited time
Aug 18, 2024 04:27 PM
UUID
icon
🗒️发表的笔记
URL CK
ErrorCheck
ErrorCheck
Description

1. Node

在ROS的世界里,最小的进程单元就是节点(node)。一个软件包里可以有多个可执行文件,可执行文件在运行之后就成了一个进程(process),这个进程在ROS中就叫做节点。 从程序角度来说,node就是一个可执行文件(通常为C++编译生成的可执行文件、Python脚本)被执行,加载到了内存之中;从功能角度来说,通常一个node负责者机器人的某一个单独的功能。由于机器人的功能模块非常复杂,我们往往不会把所有功能都集中到一个node上,而会采用分布式的方式,把鸡蛋放到不同的篮子里。例如有一个node来控制底盘轮子的运动,有一个node驱动摄像头获取图像,有一个node驱动激光雷达,有一个node根据传感器信息进行路径规划……这样做可以降低程序发生崩溃的可能性,试想一下如果把所有功能都写到一个程序中,模块间的通信、异常处理将会很麻烦。

2. Master

由于机器人的元器件很多,功能庞大,因此实际运行时往往会运行众多的node,负责感知世界、控制运动、决策和计算等功能。那么如何合理的进行调配、管理这些node?这就要利用ROS提供给我们的节点管理器master, master在整个网络通信架构里相当于管理中心,管理着各个node。node首先在master处进行注册,之后master会将该node纳入整个ROS程序中。node之间的通信也是先由master进行“牵线”,才能两两的进行点对点通信。当ROS程序启动时,第一步首先启动master,由节点管理器处理依次启动node。

3. 启动master和node

当我们要启动ROS时,首先输入命令:
此时ROS master启动,同时启动的还有**rosout parameter server,其中 rosout负责日志输出的一个节点**,其作用是告知用户当前系统的状态,包括输出系统的error、warning等等,并且将log记录于日志文件中,parameter server即是参数服务器,它并不是一个node,而是存储参数配置的一个服务器,后文我们会单独介绍。每一次我们运行ROS的节点前,都需要把master启动起来,这样才能够让节点启动和注册。
master之后,节点管理器就开始按照系统的安排协调进行启动具体的节点。节点就是一个进程,只不过在ROS中它被赋予了专用的名字里——node。在第二章我们介绍了ROS的文件系统,我们知道一个package中存放着可执行文件,可执行文件是静态的,当系统执行这些可执行文件,将这些文件加载到内存中,它就成为了动态的node。具体启动node的语句是:
通常我们运行ROS,就是按照这样的顺序启动,有时候节点太多,我们会选择用launch文件来启动,下一小节会有介绍。 Master、Node之间以及Node之间的关系如下图所示:
notion image

4. rosrun和rosnode命令

rosrun命令的详细用法如下
rosrun将会寻找PACKAGE下的名为EXECUTABLE的可执行程序,将可选参数ARGS传入。 例如在GDB下运行ros程序:
rosnode命令的详细作用列表如下
rosnode命令
作用
rosnode list
列出当前运行的node信息
rosnode info node_name
显示出node的详细信息
rosnode kill node_name
结束某个node
rosnode ping
测试连接节点
rosnode machine
列出在特定机器或列表机器上运行的节点
rosnode cleanup
清除不可到达节点的注册信息
以上命令中常用的为前三个,在开发调试时经常会需要查看当前node以及node信息,所以请记住这些常用命令。如果你想不起来,也可以通过 rosnode help来查看 rosnode命令的用法。

5. launch文件

5.1. 简介

机器人是一个系统工程,通常一个机器人运行操作时要开启很多个node,对于一个复杂的机器人的启动操作应该怎么做呢?当然,我们并不需要每个节点依次进行rosrun,ROS为我们提供了一个命令能一次性启动master和多个node。该命令是:
roslaunch命令首先会自动进行检测系统的roscore有没有运行,也即是确认节点管理器是否在运行状态中,如果master没有启动,那么roslaunch就会首先启动master,然后再按照launch的规则执行。launch文件里已经配置好了启动的规则。 所以 roslaunch就像是一个启动工具,能够一次性把多个节点按照我们预先的配置启动起来,减少我们在终端中一条条输入指令的麻烦。

5.2. 写法与格式

launch文件同样也遵循着xml格式规范,是一种标签文本,它的格式包括以下标签:

5.3. 示例

launch文件的写法和格式看起来内容比较复杂,我们先来介绍一个最简单的例子如下:
这是官网给出的一个最小的例子,文本中的信息是,它启动了一个单独的节点 talker,该节点是包 rospy_tutorials软件包中的节点。
然而实际中的launch文件要复杂很多,我们以 Ros-Academy-for-Beginners中的 robot_sim_demo为例:
这个launch文件相比上一个简单的例子来说,内容稍微有些复杂,它的作用是:启动gazebo模拟器,导入参数内容,加入机器人模型。

5.4. 小结

对于初学者,我们不要求掌握每一个标签是什么作用,但至少应该有一个印象。如果我们要进行自己写launch文件,可以先从改launch文件的模板入手,基本可以满足普通项目的要求。

6. Topic

6.1. 简介

ROS的通信方式是ROS最为核心的概念,ROS系统的精髓就在于它提供的通信架构。ROS的通信方式有以下四种:
  • Topic 主题
  • Service 服务
  • Parameter Service 参数服务器
  • Actionlib 动作库

6.2. Topic

ROS中的通信方式中,topic是常用的一种。对于实时性、周期性的消息,使用topic来传输是最佳的选择。topic是一种点对点的单向通信方式,这里的“点”指的是node,也就是说node之间可以通过topic方式来传递信息。topic要经历下面几步的初始化过程:首先,publisher节点和subscriber节点都要到节点管理器进行注册,然后publisher会发布topic,subscriber在master的指挥下会订阅该topic,从而建立起sub-pub之间的通信。**注意整个过程是单向的。**其结构示意图如下:
notion image
Subscriber接收消息会进行处理,一般这个过程叫做回调(Callback)。所谓回调就是提前定义好了一个处理函数(写在代码中),当有消息来就会触发这个处理函数,函数会对消息进行处理。
上图就是ROS的topic通信方式的流程示意图。topic通信属于一种异步的通信方式。下面我们通过一个示例来了解下如何使用topic通信。

6.3. 通信示例

参考下图,我们以摄像头画面的发布、处理、显示为例讲讲topic通信的流程。在机器人上的摄像头拍摄程序是一个node(圆圈表示,我们记作node1),当node1运行启动之后,它作为一个Publisher就开始发布topic。比如它发布了一个topic(方框表示),叫做 /camera_rgb,是rgb颜色信息,即采集到的彩色图像。同时,node2假如是图像处理程序,它订阅了 /camera_rgb这个topic,经过节点管理器的介绍,它就能建立和摄像头节点(node1)的连接。
那么怎么样来理解**“异步”**这个概念呢?在node1每发布一次消息之后,就会继续执行下一个动作,至于消息是什么状态、被怎样处理,它不需要了解;而对于node2图像处理程序,它只管接收和处理 /camera_rgb上的消息,至于是谁发来的,它不会关心。所以node1、node2两者都是各司其责,不存在协同工作,我们称这样的通信方式是异步的。
notion image
ROS是一种分布式的架构,**一个topic可以被多个节点同时发布,也可以同时被多个节点接收。**比如在这个场景中用户可以再加入一个图像显示的节点,我们在想看看摄像头节点的画面,则可以用自己的笔记本连接到机器人上的节点管理器,然后在自己的电脑上启动图像显示节点。
这就体现了分布式系统通信的好处:扩展性好、软件复用率高。
总结三点
  1. topic通信方式是异步的,发送时调用publish()方法,发送完成立即返回,不用等待反馈。
  1. subscriber通过回调函数的方式来处理消息。
  1. topic可以同时有多个subscribers,也可以同时有多个publishers。ROS中这样的例子有:/rosout、/tf等等。

6.4. 操作命令

在实际应用中,我们应该熟悉topic的几种使用命令,下表详细的列出了各自的命令及其作用。
命令
作用
rostopic list
列出当前所有的topic
rostopic info topic_name
显示某个topic的属性信息
rostopic echo topic_name
显示某个topic的内容
rostopic pub topic_name ...
向某个topic发布内容
rostopic bw topic_name
查看某个topic的带宽
rostopic hz topic_name
查看某个topic的频率
rostopic find topic_type
查找某个类型的topic
rostopic type topic_name
查看某个topic的类型(msg)
如果你一时忘记了命令的写法,可以通过 rostopic helprostopic command -h查看具体用法。

6.5. 测试实例

  1. 首先打开 ROS-Academy-for-Beginners的模拟场景,输入 roslaunch robot_sim_demo robot_spawn_launch,看到我们仿真的模拟环境。该 launch文件启动了模拟场景、机器人。
  1. 查看当前模拟器中存在的topic,输入命令 rostopic list。可以看到许多topic,它们可以视为模拟器与外界交互的接口。
  1. 查询topic /camera/rgb/image_raw的相关信息:rostopic info /camera/rgb/image_raw。则会显示类型信息type,发布者和订阅者的信息。
  1. 上步我们在演示中可以得知,并没有订阅者订阅该主题,我们指定 image_view来接收这个消息,运行命令 rosrun image_view image_view image:= [transport]。我们可以看到message,即是上一步中的type。
  1. 同理我们可以查询摄像头的深度信息depth图像。
  1. 在用键盘控制仿真机器人运动的时候,我们可以查看速度指令topic的内容 rostopic echo /cmd_vel ,可以看到窗口显示的各种坐标参数在不断的变化。
通过这些实例的测试,帮助我们更快的掌握topic各种操作命令的使用,以及对topic通信的理解。

6.6. 小结

topic的通信方式是ROS中比较常见的单向异步通信方式,它在很多时候的通信是比较易用且高效的。但是有些需要交互的通信时该方式就显露出自己的不足之处了,后续我们会介绍双向同步的通信方式service。

7. Message

7.1. 简介

topic有很严格的格式要求,比如上节的摄像头进程中的rgb图像topic,它就必然要遵循ROS中定义好的rgb图像格式。这种数据格式就是Message。**Message按照定义解释就是topic内容的数据类型,也称之为topic的格式标准。**这里和我们平常用到的Massage直观概念有所不同,这里的Message不单单指一条发布或者订阅的消息,也指定为topic的格式标准。

7.2. 结构与类型

基本的msg包括**bool、int8、int16、int32、int64(以及uint)、float、float64、string、time、duration、header、可变长数组array[]、固定长度数组array[C]。**那么具体的一个msg是怎么组成的呢?我们用一个具体的msg来了解,例如上例中的msg sensor_msg/image,位置存放在 sensor_msgs/msg/image.msg里,它的结构如下:
观察上面msg的定义,**是不是很类似C语言中的结构体呢?**通过具体的定义图像的宽度,高度等等来规范图像的格式。所以这就解释了Message不仅仅是我们平时理解的一条一条的消息,而且更是ROS中topic的格式规范。或者可以理解msg是一个“类”,那么我们每次发布的内容可以理解为“对象”,这么对比来理解可能更加容易。 我们实际通常不会把Message概念分的那么清,**通常说Message既指的是类,也是指它的对象。**而msg文件则相当于类的定义了。

7.3. 操作命令

rosmsg的命令相比topic就比较少了,只有两个如下:
rosmsg命令
作用
rosmsg list
列出系统上所有的msg
rosmsg show msg_name
显示某个msg的内容

7.4. 常见message

我们看看常见的message类型,包括std_msgs, sensor_msgs, nav_msgs, geometry_msgs等
Vector3.msg
Accel.msg
Header.msg
Echos.msg
Quaternion.msg
Imu.msg
LaserScan.msg
Point.msg
Pose.msg
PoseStamped.msg
PoseWithCovariance.msg
Power.msg
Twist.msg
TwistWithCovariance.msg
Odometry.msg

8. Service

8.1. Service

我们介绍了ROS的通信方式中的topic(主题)通信,我们知道topic是ROS中的一种单向的异步通信方式。然而有些时候单向的通信满足不了通信要求,比如当一些节点只是临时而非周期性的需要某些数据,如果用topic通信方式时就会消耗大量不必要的系统资源,造成系统的低效率高功耗。 这种情况下,**就需要有另外一种请求-查询式的通信模型。**这节我们来介绍ROS通信中的另一种通信方式——service(服务)。

8.2. 工作原理

8.2.1. 简介

为了解决以上问题,service方式在通信模型上与topic做了区别。Service通信是双向的,它不仅可以发送消息,同时还会有反馈。所以service包括两部分,一部分是请求方(Clinet),另一部分是应答方/服务提供方(Server)。这时请求方(Client)就会发送一个request,要等待server处理,反馈回一个reply,这样通过类似“请求-应答”的机制完成整个服务通信。
这种通信方式的示意图如下: Node B是server(应答方),提供了一个服务的接口,叫做
,我们一般都会用string类型来指定service的名称,类似于topic。Node A向Node B发起了请求,经过处理后得到了反馈。
notion image

8.2.2. 过程

Service是同步通信方式,所谓同步就是说,此时Node A发布请求后会在原地等待reply,直到Node B处理完了请求并且完成了reply,Node A才会继续执行。**Node A等待过程中,是处于阻塞状态的成通信。**这样的通信模型没有频繁的消息传递,没有冲突与高系统资源的占用,只有接受请求才执行服务,简单而且高效。

8.3. topic VS service

我们对比一下这两种最常用的通信方式,加深我们对两者的理解和认识,具体见下表:
名称
Topic
Service
通信方式
异步通信
同步通信
实现原理
TCP/IP
TCP/IP
通信模型
Publish-Subscribe
Request-Reply
映射关系
Publish-Subscribe(多对多)
Request-Reply(多对一)
特点
接受者收到数据会回调(Callback)
远程过程调用(RPC)服务器端的服务
应用场景
连续、高频的数据发布
偶尔使用的功能/具体的任务
举例
激光雷达、里程计发布数据
开关传感器、拍照、逆解计算
  • *注意:**远程过程调用(Remote Procedure Call,RPC),可以简单通俗的理解为在一个进程里调用另一个进程的函数。

8.4. 操作命令

在实际应用中,service通信方式的命令 rosservice,具体的命令参数如下表:
rosservice 命令
作用
rosservice list
显示服务列表
rosservice info
打印服务信息
rosservice type
打印服务类型
rosservice uri
打印服务ROSRPC uri
rosservice find
按服务类型查找服务
rosservice call
使用所提供的args调用服务
rosservice args
打印服务参数

8.5. 测试实例

  1. 首先依然是打开我们教材的模拟场景 roslaunch robot_sim_demo robot_spawn.launch
  1. 输入 rosservice list,查看当前运行的服务。
  1. 随机选择 /gazebo/delete_light服务,观察名称,是删除光源的操作。
  1. 输入 rosservice info /gazebo/delete_light查看属性信息。可以看到信息,Node:/gazebo,Type:gazebo_msgs/DeleteLight, Args:Light_name。这里的类型type也就是下文介绍的srv,传递参数Light_name
  1. 输入 rosservice call /gazebo/delete_light sun,这里的sun 是参数名,使我们模拟场景中的唯一光源太阳。操作完成后可以看到场景中的光线消失。
  1. 可以看到终端的回传信息:success: True和sun successfully deleted。这就是双向通信的信息反馈,通知操作已经成功完成。

8.6. 小结

本节我们详细介绍了service通信方式,建议与topic通信方式进行对比记忆,这样我们能更深的理解这两种通信方式,也能在以后的学习工作中更加合理使用每个通信方式,获得更高的效率。

9. Srv

9.1. 简介

类似msg文件,srv文件是用来描述service服务的文件 (service的数据类型,service通信的数据格式定义在*.srv中) 它声明了一个服务,包括请求 (request) 和响应 (reply) 两部分。其格式声明如下:
举例:
msgs_demo/srv/DetectHuman.srv
msgs_demo/msg/HumanPose.msg
msgs_demo/msg/JointPose.msg
DetectHUman.srv文件为例,该服务例子取自OpenNI的人体检测ROS软件包。它是用来查询当前深度摄像头中的人体姿态和关节数的。srv文件格式很固定,第一行是请求的格式,中间用**---**隔开,第三行是应答的格式。在本例中,请求为是否开始检测,应答为一个数组,数组的每个元素为某个人的姿态(HumanPose)。而对于人的姿态,其实是一个msg,所以srv可以嵌套msg在其中,但它不能嵌套srv。

9.2. 操作命令

具体的操作指令如下表:
rossrv 命令
作用
rossrv show
显示服务描述
rossrv list
列出所有服务
rossrv md5
显示服务md5sum
rossrv package
列出包中的服务
rossrv packages
列出包含服务的包

9.3. 修改部分文件

定义完了msg、srv文件,还有重要的一个步骤就是修改package.xml和修改CMakeList.txt。这些文件需要添加一些必要的依赖等,例如:
上述文本中“**”所引就是新添加的依赖。又例如:
  • *添加的这些内容指定了srv或者msg在编译或者运行中需要的依赖。**具体的作用我们初学者可不深究,我们需要了解的是,无论我们自定义了srv,还是msg,修改上述部分添加依赖都是必不可少的一步。

10. Parameter server

10.1. 简介

前文介绍了ROS中常见的两种通信方式——主题和服务,这节介绍另外一种通信方式——参数服务器(parameter server)。与前两种通信方式不同,参数服务器也可以说是特殊的“通信方式”。特殊点在于参数服务器是节点存储参数的地方、用于配置参数,全局共享参数。参数服务器使用互联网传输,在节点管理器中运行,实现整个通信过程。
参数服务器,作为ROS中另外一种数据传输方式,有别于topic和service,它更加的静态。参数服务器维护着一个数据字典,字典里存储着各种参数和配置。

10.1.1. 字典简介

何为字典,其实就是一个个的键值对,我们小时候学习语文的时候,常常都会有一本字典,当遇到不认识的字了我们可以查部首查到这个字,获取这个字的读音、意义等等,而这里的字典可以对比理解记忆。键值kay可以理解为语文里的“部首”这个概念,每一个key都是唯一的,参照下图:
notion image
每一个key不重复,且每一个key对应着一个value。也可以说字典就是一种映射关系,在实际的项目应用中,因为字典的这种静态的映射特点,我们往往将一些不常用到的参数和配置放入参数服务器里的字典里,这样对这些数据进行读写都将方便高效。

10.1.2. 维护方式

参数服务器的维护方式非常的简单灵活,总的来讲有三种方式
  • 命令行维护
  • launch文件内读写
  • node源码
下面我们来一一介绍这三种维护方式。

10.2. 命令行维护

使用命令行来维护参数服务器,主要使用 rosparam语句来进行操作的各种命令,如下表:
rosparam 命令
作用
rosparam set param_key param_value
设置参数
rosparam get param_key
显示参数
rosparam load file_name
从文件加载参数
rosparam dump file_name
保存参数到文件
rosparam delete
删除参数
rosparam list
列出参数名称

10.2.1. load&&dump文件

load和dump文件需要遵守YAML格式,YAML格式具体示例如下:
简明解释。就是“名称+:+值”这样一种常用的解释方式。一般格式如下:
遵循格式进行定义参数。其实就可以把YAML文件的内容理解为字典,因为它也是键值对的形式。

10.3. launch文件内读写

launch文件中有很多标签,而与参数服务器相关的标签只有两个,一个是 ,另一个是。这两个标签功能比较相近,但``一般只设置一个参数,请看下例:
(1) (2) (3)
观察上例比如序号3的param就定义了一个key和一个value,交给了参数服务器维护。而序号1的param只给出了key,没有直接给出value,这里的value是由后没的脚本运行结果作为value进行定义的。序号(2)就是rosparam的典型用法,先指定一个YAML文件,然后施加command,其效果等于 rosparam load file_name

10.4. node源码

除了上述最常用的两种读写参数服务器的方法,还有一种就是修改ROS的源码,也就是利用API来对参数服务器进行操作。具体内容我们学习完后面章节再进行介绍。

10.5. 操作实例

  1. 首先依然是打开我们教材的模拟场景 roslaunch robot_sim_demo robot_spawn.launch
  1. 输入 rosparam list查看参数服务器上的param。
  1. 查询参数信息,例如查询竖直方向重力参数。输入 rosparam get /gazebo/gravity_z回车得到参数值value=-9.8。
  1. 尝试保存一个参数到文件输入 rosparam dump param.yaml之后就可以在当前路径看到该文件,也就能打开去查看到相关的参数信息。
  1. 参数服务器的其他命令操作方式大致相同,我们可以多多练习,巩固对参数服务器的理解和应用。

10.6. 参数类型

ROS参数服务器为参数值使用XMLRPC数据类型,其中包括:strings, integers, floats, booleans, lists, dictionaries, iso8601 dates, and base64-encoded data。

11. Action

11.1. 简介

Actionlib是ROS中一个很重要的库,**类似service通信机制,actionlib也是一种请求响应机制的通信方式,**actionlib主要弥补了service通信的一个不足,就是当机器人执行一个长时间的任务时,假如利用service通信方式,那么publisher会很长时间接受不到反馈的reply,致使通信受阻。当service通信不能很好的完成任务时候,actionlib则可以比较适合实现长时间的通信过程,actionlib通信过程可以随时被查看过程进度,也可以终止请求,这样的一个特性,使得它在一些特别的机制中拥有很高的效率。

12. 通信原理

Action的工作原理是client-server模式,也是一个双向的通信模式。通信双方在ROS Action Protocol下通过消息进行数据的交流通信。client和server为用户提供一个简单的API来请求目标(在客户端)或通过函数调用和回调来执行目标(在服务器端)。
工作模式的结构示意图如下:
notion image
通信双方在ROS Action Protocal下进行交流通信是通过接口来实现,如下图:
notion image
我们可以看到,客户端会向服务器发送目标指令和取消动作指令,而服务器则可以给客户端发送实时的状态信息,结果信息,反馈信息等等,从而完成了service没法做到的部分.

12.1. Action 规范

利用动作库进行请求响应,动作的内容格式应包含三个部分,目标、反馈、结果。
  • 目标
机器人执行一个动作,应该有明确的移动目标信息,包括一些参数的设定,方向、角度、速度等等。从而使机器人完成动作任务。
  • 反馈
在动作进行的过程中,应该有实时的状态信息反馈给服务器的实施者,告诉实施者动作完成的状态,可以使实施者作出准确的判断去修正命令。
  • 结果
当运动完成时,动作服务器把本次运动的结果数据发送给客户端,使客户端得到本次动作的全部信息,例如可能包含机器人的运动时长,最终姿势等等。

12.2. Action规范文件格式

Action规范文件的后缀名是.action,它的内容格式如下:

12.3. Action实例详解

Actionlib是一个用来实现action的一个功能包集。我们在demo中设置一个场景,执行一个搬运的action,搬运过程中客户端会不断的发回反馈信息,最终完成整个搬运过程.
本小节的演示源码在课程的演示代码包里,此处为链接.
首先写handling.action文件,类比如上的格式.包括三个部分,目标,结果,反馈.如下:
写完之后修改文件夹里CmakeLists.txt如下内容:
  1. find_package(catkin REQUIRED genmsg actionlib_msgs actionlib)
  1. add_action_files(DIRECTORY action FILES DoDishes.action) generate_messages(DEPENDENCIES actionlib_msgs)
  1. add_action_files(DIRECTORY action FILES Handling.action)
  1. generate_messages( DEPENDENCIES actionlib_msgs)
修改package.xml,添加所需要的依赖如下:
  1. actionlib
  1. actionlib_msgs
  1. actionlib
  1. actionlib_msgs
然后回到工作空间 catkin_ws进行编译.
本例中设置的的action,定义了一个搬运的例子,首先写客户端,实现功能发送action请求,包括进行目标活动,或者目标活动.之后写服务器,实验返回客户端活动当前状态信息,结果信息,和反馈信息.从而实现action.本例测试结果截图如下:
notion image
notion image
notion image
notion image

12.4. 小结

至此,ROS通信架构的四种通信方式就介绍结束,我们可以对比学习这四种通信方式,去思考每一种通信的优缺点和适用条件,在正确的地方用正确的通信方式,这样整个ROS的通信会更加高效,机器人也将更加的灵活和智能。机器人学会了通信,也就相当于有了“灵魂”。

13. 常见srv类型

本小节介绍常见的srv类型及其定义 srv类型相当于两个message通道,一个发送,一个接收
AddTwoInts.srv
Empty.srv
GetMap.srv
GetPlan.srv
SetBool.srv
SetCameraInfo.srv
SetMap.srv
TalkerListener.srv
Trigger.srv

14. 常见action类型

本小节介绍常见的action类型以及其定义
AddTwoInts.action
AutoDocking.action
GetMap.action
MoveBase.action
上一篇
ROS学习过程的疑问
下一篇
ROS入门

评论
Loading...