ROS全称Robot Operating System,是一个用于机器人应用开发的开源框架。ROS提供了一系列工具和库,使得机器人开发者可以更轻松地编写软件,从而实现机器人的感知、控制、定位、导航等功能。ROS使用publisher-subscriber模型实现消息传递,支持多种编程语言。由于它拥有方便的模块化设计,可以让开发者更方便地调试和修改程序,大大缩短机器人应用的开发时间。ROS在机器人领域得到了广泛应用,也成为了学术界和工业界的标准工具之一。
机器人开发的分工思想,实现了不同研发团队间的共享和协作,提升了机器人的研发效率,为了服务“ 分工”,ROS主要设计了如下目标:
代码复用:ROS的目标不是成为具有最多功能的框架,ROS的主要目标是支持机器人技术研发中的代码重用。
分布式:ROS是进程(也称为Nodes)的分布式框架,ROS中的进程可分布于不同主机,不同主机协同工作,从而分散计算压力
松耦合:ROS中功能模块封装于独立的功能包或元功能包,便于分享,功能包内的模块以节点为单位运行,以ROS标准的IO作为接口,开发者不需要关注模块内部实现,只要了解接口规则就能实现复用,实现了模块间点对点的松耦合连接
精简:ROS被设计为尽可能精简,以便为ROS编写的代码可以与其他机器人软件框架一起使用。ROS易于与其他机器人软件框架集成:ROS已与OpenRAVE,Orocos和Player集成。
语言独立性:包括Java,C++,Python等。为了支持更多应用开发和移植,ROS设计为一种语言弱相关的框架结构,使用简洁,中立的定义语言描述模块间的消息接口,在编译中再产生所使用语言的目标文件,为消息交互提供支持,同时允许消息接口的嵌套使用
易于测试:ROS具有称为rostest的内置单元/集成测试框架,可轻松安装和拆卸测试工具。
大型应用:ROS适用于大型运行时系统和大型开发流程。
丰富的组件化工具包:ROS可采用组件化方式集成一些工具和软件到系统中并作为一个组件直接使用,如RVIZ(3D可视化工具),开发者根据ROS定义的接口在其中显示机器人模型等,组件还包括仿真环境和消息查看工具等
免费且开源:开发者众多,功能包多
ROS是一个由来已久、贡献者众多的大型软件项目。在ROS诞生之前,很多学者认为,机器人研究需要一个开放式的协作框架,并且已经有不少类似的项目致力于实现这样的框架。在这些工作中,斯坦福大学在2000年年中开展了一系列相关研究项目,如斯坦福人工智能机器人(STandford AI Robot, STAIR)项目、个人机器人(Personal Robots, PR)项目等,在上述项目中,在研究具有代表性、集成式人工智能系统的过程中,创立了用于室内场景的高灵活性、动态软件系统,其可以用于机器人学研究。
ROS的发行版本(ROS distribution)指ROS软件包的版本,其与Linux的发行版本(如Ubuntu)的概念类似。推出ROS发行版本的目的在于使开发人员可以使用相对稳定的代码库,直到其准备好将所有内容进行版本升级为止。因此,每个发行版本推出后,ROS开发者通常仅对这一版本的bug进行修复,同时提供少量针对核心软件包的改进。
版本特点按照英文字母顺序命名,ROS 目前已经发布了ROS1 的终极版本: noetic,并建议后期过渡至 ROS2 版本。noetic 版本之前默认使用的是 Python2,noetic 支持 Python3。建议版本: noetic 或 melodic 或 kinetic
总的来说,自2007年以来,ROS已经在机器人领域飞速发展,并且成为了最受欢迎的机器人操作系统之一,也得到了学术界和工业界的广泛应用。
ROS中涉及的编程语言以C++和Python为主,ROS中的大多数程序两者都可以实现。ROS 当前的代码统计量,总行数超过 1400 万,作者超过2477 名。代码语言以C++为主,63.98%的代码是用 C++编写的,排名第二的是 python,占13.57%,可以说ROS 基本上都是使用这两种语言,来实现大部分的功能。
在本系列教程中,每一个案例也都会分别使用C++和Python两种方案演示,大家可以根据自身情况选择合适的实现方案。ROS中的程序即便使用不同的编程语言,实现流程也大致类似,以当前HelloWorld程序为例,实现流程大致如下:
在ROS中实现HelloWorld程序,可以采取以下大致流程:
创建ROS工作空间(ROS workspace):首先需要创建ROS工作空间来承载本次工程。
创建一个ROS功能包(ROS package):在工作空间中创建一个新的ROS包,这个包将包含我们的代码和依赖包。
编写ROS节点:在ROS包中创建ROS节点,这个节点将会发送Hello World消息。
编译和构建(build)ROS包:将ROS节点打包成一个可执行的二进制文件。
运行ROS节点:在终端中运行ROS节点,此时ROS节点将会发送Hello World消息。
详细实现流程可以参考官方文档或者教程,这里只是一个大致的流程概述。虽然实现同一功能时,C++和Python可以互换,但是具体选择哪种语言,需要视需求而定,因为两种语言相较而言:C++运行效率高但是编码效率低,而Python则反之,基于二者互补的特点,ROS设计者分别设计了roscpp与rospy库,前者旨在成为ROS的高性能库,而后者则一般用于对性能无要求的场景,旨在提高开发效率。
创建一个工作空间以及一个 src 子目录,然后再进入工作空间调用 catkin_make命令编译。
mkdir -p 自定义空间名称/src
cd 自定义空间名称
catkin_make
在工作空间下生成一个功能包,该功能包依赖于 roscpp、rospy 与 std_msgs,其中roscpp是使用C++实现的库,而rospy则是使用python实现的库,std_msgs是标准消息库,创建ROS功能包时,一般都会依赖这三个库实现
cd src
catkin_create_pkg 自定义ROS包名 roscpp rospy std_msgs
假设你已经创建了ROS的工作空间,并且创建了ROS的功能包,那么就可以进入核心步骤了,使用C++编写程序实现:
进入 ros 包的 src 目录编辑源文件后,C++源码实现(文件名自定义)
// 导入ROS的头文件
#include "ros/ros.h"
int main(int argc, char *argv[])
{
// 初始化ROS节点
ros::init(argc, argv, "hello_world");
// 创建句柄
ros::NodeHandle nh;
// 输出Hello World!
ROS_INFO("Hello World!");
return 0;
}
编辑 ros 包下的 Cmakelist.txt文件
add_executable(步骤3的源文件名
src/步骤3的源文件名.cpp
)
target_link_libraries(步骤3的源文件名
${catkin_LIBRARIES}
)
进入工作空间目录并编译
cd 自定义空间名称
catkin_make
生成 build devel ....
执行
先启动命令行1:
roscore
再启动命令行2:
cd 工作空间
source ./devel/setup.bash
rosrun 包名 C++节点
假设你已经创建了ROS的工作空间,并且创建了ROS的功能包,那么就可以进入核心步骤了,使用Python编写程序实现:
进入 ros 包添加 scripts 目录并编辑 python 文件
cd ros包
mkdir scripts
新建 python 文件: (文件名自定义)
#! /usr/bin/env python
"""
Python 版 HelloWorld
"""
import rospy
if __name__ == "__main__":
rospy.init_node("Hello")
rospy.loginfo("Hello World!!!!")
为 python 文件添加可执行权限
chmod +x 自定义文件名.py
编辑 ros 包下的 CamkeList.txt 文件
catkin_install_python(PROGRAMS scripts/自定义文件名.py
DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
进入工作空间目录并编译
cd 自定义空间名称
catkin_make
进入工作空间目录并执行
先启动命令行1:
roscore
再启动命令行2:
cd 工作空间
source ./devel/setup.bash
rosrun 包名 自定义文件名.py
输出结果:Hello World!!!
ROS采用的是分布式系统结构,由多个进程(节点)组成,其中各个节点之间通过ROS通信机制(topic、Service)来进行交互。这些节点可以部署在同一台机器上,也可以部署在不同机器上,还可以部署在互联网上。下面是ROS架构中涉及到的主要组件:
节点(Node):是ROS架构中的基本组件,通常指一个运行着的进程,负责完成特定的任务。每个节点可以有多个ROS里程碑。ROS 提供了处理节点的工具,用于节点信息、状态、可用性等的查询操作,例如可以用下面的命令对正在运行的节点进行操作。
主节点(Master):是ROS系统的重要组件,负责维护运行节点的注册,管理通信连接和服务请求等,同时也提供一些监控和调试功能。
参数服务器(Parameter Server):是ROS中的全局参数管理组件,可以在不同节点之间共享参数数据,提高了程序的复用性和可扩展性。参数服务器能够使数据通过关键词存储在一个系统的核心位置。通过使用参数,就能够在节点运行时动态配置节点或改变节点的工作任务。参数服务器是可通过网络访问的共享的多变量字典,节点使用此服务器来存储和检索运行时的参数。ROS 中关于参数服务器的命令行工具,请看下面的常用命令。
话题(Topic):是ROS中的一种发布/订阅模式的通信机制,用于实现节点之间的数据交互和数据传输。ROS 提供了操作主题的命令工具,这里列举出一些常用的命令。
服务(Service):是ROS中的一种请求/响应模式的通信机制,用于实现节点之间的函数调用和参数传递。
ROS 提供了操作服务的命令工具,这里列举出一些常用的命令。
行为(Action):是ROS中的一种高级通信机制,用于实现节点之间复杂的交互行为,例如控制机器人完成一系列动作。
消息(Message):是ROS中的一种数据结构格式,用于在话题、服务和行为等通信机制中传输数据。ROS 提供了获取消息相关信息的命令工具,这里列举出一些常用的命令,来具体看看吧。
ROS工具(ROS Tools):ROS提供了一系列便于开发、调试和管理的工具,例如rqt、rqt_plot、rqt_graph、rviz等。
此外,ROS还具有丰富的核心模块。 其核心模块包括:通信结构基础、机器人特性功能、工具集。通信结构基础主要是消息传递、记录回放消息、远程过程调用、分布式参数系统;机器人特性功能主要是标准机器人消息、机器人几何库、机器人描述语言、抢占式远程过程调用、诊断、位置估计、定位导航;工具集主要是命令式工具、可视化工具、图形化接口。
ROS 核心工具很丰富,ROS 常用命令工具是 rostopic、rosservice、rosnode、rosparam、rosmsg、rossrv、roswtf;ROS 常用可视化工具是 rqt、rviz;ROS 用于存储与回放数据的工具rosbag;ROS 的 log 系统记录软件运行的相关信息;ROS 还拥有强大的第三方工具支持:三维仿真环境 Gazebo、计算机视觉库 OpenCV、点云库 PCL、机械臂控制库MoveIt、工业应用库 Industrial、机器人编程工具箱 MRPT、实时控制库 Orocos。
以上就是ROS架构中的主要组件,ROS 的核心概念主要是节点和用于节点间通信的话题与服务。它们协同工作,构成了强大而灵活的ROS系统,可以支持各种种类的机器人和应用领域。一些常见的ROS应用领域包括机器人控制、自动驾驶、智能家居、医疗健康等。
ROS 文件系统是 ROS 架构的关键部分之一,被称为包(package)。包文件夹包含 ROS 节点、主题、服务、参数和其他机器人相关的文件。每个包可以具有子目录来组织它的组件。ROS 节点可以跨包通信。
ROS 文件系统的基本结构如下:
工作空间是一个包含功能包、可编辑源文件和编译包的文件夹,当你想同时编译不同的功能包时非常有用,并且可以保存本地开发包。当然,用户可以根据自己的需要创建多个工作空间,在每个工作空间中开发不同用途的功能包。不过作为学习,我们先以一个工作空间为例。如上图 ,创建了一个名为 catkin_ws 的工作空间,该工作空间下会有3 个文件夹:src、build、devel。
在ROS中,功能包是一种用于组织、管理和共享ROS程序和相关资源的标准化机制。一个ROS功能包可以包含ROS程序的最小工作单元,通常包括ROS的进程(节点)、发布和订阅的消息、服务定义、参数文件以及其他必要文件和资源。功能包是ROS强大和灵活的架构的基本组成部分之一。如上图,一个功能包中主要包含这几个文件:
消息是 ROS 中一个进程(节点)发送到其他进程(节点)的信息,消息类型是消息的数据结构,ROS 系统提供了很多标准类型的消息可以直接使用,如果你要使用一些非标准类型的消息,就需要自己来定义该类型的消息。
ROS 使用了一种精简的消息类型描述语言来描述 ROS 进程(节点)发布的数据值。通过这种描述语言对消息类型的定义,ROS 可以在不同的编程语言(如c++、python 等)书写的程序中使用此消息。不管是 ROS 系统提供的标准类型消息,还是用户自定义的非标准类型消息,定义文件都是以*.msg 作为扩展名。消息类型的定义分为两个主要部分:字段的数据类型和字段的名称,简单点说就是结构体中的变量类型和变量名称。比如下面的一个示例消息定义文件 example.msg 的内容,如上图 ,int32、float32、string 就是字段的数据类型,id、vel、name 就是字段的名称。
话题通信是ROS中使用频率最高的一种分布式通信模式,用于传输实时消息。话题是一种数据流对象,它具有订阅-发布模型,允许节点之间通过它们之间异步的消息传输进行通信。也即:一个节点发布消息,另一个节点订阅该消息。
像雷达、摄像头、GPS… 等等一些传感器数据的采集,也都是使用了话题通信,换言之,话题通信适用于不断更新的数据传输相关的应用场景。
是一个命名的数据通道,通过它进行的通信中传递的是消息。话题名是话题通信中最重要的部分,它被用作识别和链接的标准。该模型如下图所示,该模型中涉及到三个角色:
ROS Master 负责保管 Talker 和 Listener 注册的信息,并匹配话题相同的 Talker 与 Listener,帮助 Talker 与 Listener 建立连接,连接建立后,Talker 可以发布消息,且发布的消息会被 Listener 订阅。
Talker注册
Talker启动后,会通过RPC在 ROS Master 中注册自身信息,其中包含所发布消息的话题名称。ROS Master 会将节点的注册信息加入到注册表中。
Listener注册
Listener启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要订阅消息的话题名。ROS Master 会将节点的注册信息加入到注册表中。
ROS Master实现信息匹配
ROS Master 会根据注册表中的信息匹配Talker 和 Listener,并通过 RPC 向 Listener 发送 Talker 的 RPC 地址信息。
Listener向Talker发送请求
Listener 根据接收到的 RPC 地址,通过 RPC 向 Talker 发送连接请求,传输订阅的话题名称、消息类型以及通信协议(TCP/UDP)。
Talker确认请求
Talker 接收到 Listener 的请求后,也是通过 RPC 向 Listener 确认连接信息,并发送自身的 TCP 地址信息。
Listener与Talker件里连接
Listener 根据步骤4 返回的消息使用 TCP 与 Talker 建立网络连接。
Talker向Listener发送消息
连接建立后,Talker 开始向 Listener 发布消息。
编写发布订阅实现,要求发布方以10HZ(每秒10次)的频率发布文本消息,订阅方订阅消息并将消息内容打印输出。
流程:
需要注意的是,这是一个简单的发布者程序示例,该示例基于ROS C++客户端库编写。其中涉及到的类和库函数都需要进行正确的初始化、配置、构造和调用,需要确保在ROS系统中实现正确的发布流程和相关操作。
上图是消息发布与订阅 ROS 通信网络结构图
发布方
/*
需求: 实现基本的话题通信,一方发布数据,一方接收数据,
实现的关键点:
1.发送方
2.接收方
3.数据(此处为普通文本)
PS: 二者需要设置相同的话题
消息发布方:
循环发布信息:HelloWorld 后缀数字编号
实现流程:
1.包含头文件
2.初始化 ROS 节点:命名(唯一)
3.实例化 ROS 句柄
4.实例化 发布者 对象
5.组织被发布的数据,并编写逻辑发布数据
*/
// 1.包含头文件
#include "ros/ros.h"
#include "std_msgs/String.h" //普通文本类型的消息
#include
int main(int argc, char *argv[])
{
//设置编码
setlocale(LC_ALL,"");
//2.初始化 ROS 节点:命名(唯一)
// 参数1和参数2 后期为节点传值会使用
// 参数3 是节点名称,是一个标识符,需要保证运行后,在 ROS 网络拓扑中唯一
ros::init(argc,argv,"talker");
//3.实例化 ROS 句柄
ros::NodeHandle nh;//该类封装了 ROS 中的一些常用功能
//4.实例化 发布者 对象
//泛型: 发布的消息类型
//参数1: 要发布到的话题
//参数2: 队列中最大保存的消息数,超出此阀值时,先进的先销毁(时间早的先销毁)
ros::Publisher pub = nh.advertise<std_msgs::String>("chatter",10);
//5.组织被发布的数据,并编写逻辑发布数据
//数据(动态组织)
std_msgs::String msg;
// msg.data = "你好啊!!!";
std::string msg_front = "Hello 你好!"; //消息前缀
int count = 0; //消息计数器
//逻辑(一秒10次)
ros::Rate r(1);
//节点不死
while (ros::ok())
{
//使用 stringstream 拼接字符串与编号
std::stringstream ss;
ss << msg_front << count;
msg.data = ss.str();
//发布消息
pub.publish(msg);
//加入调试,打印发送的消息
ROS_INFO("发送的消息:%s",msg.data.c_str());
//根据前面制定的发送贫频率自动休眠 休眠时间 = 1/频率;
r.sleep();
count++;//循环结束前,让 count 自增
//暂无应用
ros::spinOnce();
}
return 0;
}
订阅方
// 1.包含头文件
#include "ros/ros.h"
#include "std_msgs/String.h"
void doMsg(const std_msgs::String::ConstPtr& msg_p){
ROS_INFO("我听见:%s",msg_p->data.c_str());
// ROS_INFO("我听见:%s",(*msg_p).data.c_str());
}
int main(int argc, char *argv[])
{
setlocale(LC_ALL,"");
//2.初始化 ROS 节点:命名(唯一)
ros::init(argc,argv,"listener");
//3.实例化 ROS 句柄
ros::NodeHandle nh;
//4.实例化 订阅者 对象
ros::Subscriber sub = nh.subscribe<std_msgs::String>("chatter",10,doMsg);
//5.处理订阅的消息(回调函数)
// 6.设置循环调用回调函数
ros::spin();//循环读取接收的数据,并调用回调函数处理
return 0;
}
配置 CMakeLists.txt
首先,add_executable用于创建可执行文件。在这个例子中,它分别创建了两个可执行文件Hello_pub和Hello_sub。src/Hello_pub.cpp和src/Hello_sub.cpp分别是两个可执行文件的源代码。
然后,target_link_libraries用于链接所需的库。${catkin_LIBRARIES}是一个变量,在ROS中指代需要链接的所有库。
这段代码的作用是在ROS的工作空间中创建两个可执行文件,并将${catkin_LIBRARIES}作为链接库进行链接。这些可执行文件可能是ROS节点或其他形式的程序。
//发布者的可执行文件的源代码
add_executable(Hello_pub
src/Hello_pub.cpp
)
//订阅者的可执行文件的源代码
add_executable(Hello_sub
src/Hello_sub.cpp
)
//发布者所需的链接库
target_link_libraries(Hello_pub
${catkin_LIBRARIES}
)
//订阅者所需的链接库
target_link_libraries(Hello_sub
${catkin_LIBRARIES}
)
#! /usr/bin/env python
"""
需求: 实现基本的话题通信,一方发布数据,一方接收数据,
实现的关键点:
1.发送方
2.接收方
3.数据(此处为普通文本)
PS: 二者需要设置相同的话题
消息发布方:
循环发布信息:HelloWorld 后缀数字编号
实现流程:
1.导包
2.初始化 ROS 节点:命名(唯一)
3.实例化 发布者 对象
4.组织被发布的数据,并编写逻辑发布数据
"""
#1.导包
import rospy
from std_msgs.msg import String
if __name__ == "__main__":
#2.初始化 ROS 节点:命名(唯一)
rospy.init_node("talker_p")
#3.实例化 发布者 对象
pub = rospy.Publisher("chatter",String,queue_size=10)
#4.组织被发布的数据,并编写逻辑发布数据
msg = String() #创建 msg 对象
msg_front = "hello 你好"
count = 0 #计数器
# 设置循环频率
rate = rospy.Rate(1)
while not rospy.is_shutdown():
#拼接字符串
msg.data = msg_front + str(count)
pub.publish(msg)
rate.sleep()
rospy.loginfo("写出的数据:%s",msg.data)
count += 1
#! /usr/bin/env python
"""
需求: 实现基本的话题通信,一方发布数据,一方接收数据,
实现的关键点:
1.发送方
2.接收方
3.数据(此处为普通文本)
消息订阅方:
订阅话题并打印接收到的消息
实现流程:
1.导包
2.初始化 ROS 节点:命名(唯一)
3.实例化 订阅者 对象
4.处理订阅的消息(回调函数)
5.设置循环调用回调函数
"""
#1.导包
import rospy
from std_msgs.msg import String
def doMsg(msg):
rospy.loginfo("I heard:%s",msg.data)
if __name__ == "__main__":
#2.初始化 ROS 节点:命名(唯一)
rospy.init_node("listener_p")
#3.实例化 订阅者 对象
sub = rospy.Subscriber("chatter",String,doMsg,queue_size=10)
#4.处理订阅的消息(回调函数)
#5.设置循环调用回调函数
rospy.spin()
catkin_install_python(PROGRAMS
scripts/talker_p.py
scripts/listener_p.py
DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
话题通信自定义msg
在 ROS 通信协议中,数据载体是一个较为重要组成部分,ROS 中通过 std_msgs 封装了一些原生的数据类型,比如:String、Int32、Int64、Char、Bool、Empty… 但是,这些数据一般只包含一个 data 字段,结构的单一意味着功能上的局限性,当传输一些复杂的数据,比如: 激光雷达的信息… std_msgs 由于描述性较差而显得力不从心,这种场景下可以使用自定义的消息类型。
在ROS中,自定义的消息类型是通过在.msg文件中定义的。该文件包含一个或多个字段的类型和名称,以及一个消息名称。在将消息定义保存到msg文件中后,您需要在您的CMakeLists.txt文件和package.xml中添加必要的信息以使消息生效。
在ROS中,自定义消息通常由不同的基本类型组成。 以下是ROS中自定义消息支持的基本类型:
Integers: 整数类型,包括byte、char、int8、int16、int32和int64。
Floating point numbers: 浮点数类型,包括float32和float64。
Boolean values: 布尔类型,只有两种取值:True或False。
Strings: 字符串类型,包括string。
Time and duration: ROS中的时间和时间间隔类型,包括time和duration。
Arrays: 数组类型,也称为 sequence,包括使用尖括号<>括起来的类型(例如:int16[])。
Fixed-length arrays: 固定长度数组类型,也称为 array,用大括号{}括起来,后面紧跟,其中n是数组长度(例如:int16{4})。
Header: ROS中的消息头,包括std_msgs/Header。包含时间戳和ROS中常用的坐标帧信息。会经常看到msg文件的第一行具有Header标头。
首先创建一个空的package单独存放msg类型(当然也可以在任意的package中自定义msg类型)这里为便于说明,建立一个名为test_msgs的包,用于对自定义msg类型的用法举例。
流程:
新建msg文件
然后在test_msgs中创建msg文件夹,在msg文件夹其中新建一个名为Test.msg消息类型文件
$ cd catkin_ws/src
$ catkin_create_pkg test_msgs
$ cd test_msgs
$ mkdir msg
Test.msg的内容
float32[] data
float32 vel
geometry_msgs/Pose pose
string name
修改package.xml
接下来需要message_generation生成C++或Python能使用的代码,需要message_runtime提供运行时的支持,所以package.xml中添加以下两句
<build_depend>message_generation</build_depend>
<run_depend>message_runtime</run_depend>
修改CMakeLists.txt
修改CMakeLists.txt
CMakeLists.txt要注意四个地方
(1)首先调用find_package查找依赖的包,必备的有roscpp rospy message_generation,其他根据具体类型添加,比如上面的msg文件中用到了geometry_msgs/Pose pose类型,那么必须查找geometry_msgs
find_package(catkin REQUIRED COMPONENTS roscpp rospy message_generation std_msgs geometry_msgs)
(2)然后是add_message_files,指定msg文件
add_message_files(
FILES
Test.msg
# Message2.msg
)
(3)然后是generate_messages,指定生成消息文件时的依赖项,比如上面嵌套了其他消息类型geometry_msgs,那么必须注明
#generate_messages必须在catkin_package前面
generate_messages(
DEPENDENCIES
geometry_msgs
)
(4)然后是catkin_package设置运行依赖
catkin_package(
CATKIN_DEPENDS message_runtime
)
编译
略
注意:要使用自定义的消息类型必须source自定义消息所在的工作空间,否则rosmsg show test_msgs/Test和rostopic echo /test_msg(/test_msg是节点中使用自定义消息类型test_msgs/Test的topic)都会报错,因为没有source的情况下自定义消息类型是不可见的,被认为是未定义类型
未完待续!