写在前面
笔者在参加机器狗比赛,故写此博客记录下学习过程。
关于ROS的学习,我主要是根据古月居ROS入门21讲的顺序和理念,加上自己查阅一些资料来学习的,如果有什么问题欢迎来交流~
Ubuntu系统的安装
由于ROS只能在linux系统下运行,而目前对ROS最好支持的linux系统是Ubuntu,故选择了安装Ubuntu系统,为了避免在自己系统上出现问题,采用虚拟机安装的形式,这里我使用的是VMware虚拟机,具体过程就是下载VMware,下载Ubuntu的iso文件,注意要选择自己需要的版本,一般选偶数版本,维护时间长,这里我用的是Ubuntu20.04,然后就是安装系统了,具体过程可以参考下面这篇文章,写的很详细,包括了Ubuntu官网和VMware官网地址。
VMware虚拟机安装Ubuntuhttps://blog.csdn.net/qq_43374681/article/details/129248167 在安装过程中可能会出现窗口太小看不到下一步的情况,这时候可以按table+alt来选中选项,按enter执行,多按几次,摸清规律就能成功进入下一步;或者我在网上看到的alt+F7(笔记本记得按Fn)也可以拖动窗口。
ROS的安装
关于ROS的安装,网上有相当多的教程,大家可以自行参考,步骤无非就那几步:添加ROS软件源,添加密钥,初始化,安装ROS,初始化rosdep,设置环境变量,安装rosinstall。下面是一个详细的安装过程:
Ubuntu20.04安装ROShttps://blog.csdn.net/qq_44339029/article/details/120579608 安装完之后需要检测是否安装成功,可以通过运行小海龟仿真程序来检测:
roscore
rosrun turtlesim turtlesim_node
rosrun turtlesim turtle_teleop_key
打开三个终端(alt+table+T)来分别运行上述三句话,第一句为roscore,在运行节点(node)之前,都需要启动roscore,来启动运行ros节点必要的ROS Master和ROS parameter。第二句话为打开海龟仿真器界面,此时会弹出一个窗口,窗口中心会有一只小海龟,第三句话表示用键盘控制小海龟的运动,此时当把鼠标指针放在第三个终端窗口中按键盘的上下左右键,就会看到小海龟动起来啦,这就表示ROS安装成功了。
VSCode下载以及环境配置
由于ROS用到的代码以C++和Python为主,这里选择VSCode来编写代码,关于它的下载安装,我参考的是下面这篇文章:
如何在Ubuntu20.04中安装VSCodehttps://zhuanlan.zhihu.com/p/137861452 安装完成之后,要配置C++和Python的环境,先介绍Python,这个比较简单,直接在扩展程序当中搜索Python直接下载安装后重启即可;对于C++则相对麻烦一点,首先同理安装C/C++和Code Runner扩展,然后重启,打开终端,输入以下命令:
sudo apt-get update
sudo apt-get install gcc
sudo apt-get install g++
sudo apt-get install gdb
注意,执行sudo语句时要输入自己的密钥,这个是不会显示的。
此时,已经可以运行cpp代码文件了,但还不能调试,为了调试,我们需要下面两个文件:launch.json和tasks.json,在不同的代码空间中调试代码都需要重新写入这两个文件,这两个文件的内容如下:
launch.json:
{
"version": "0.2.0",
"configurations": [
{
"name": "C/C++",
"type": "cppdbg",
"request": "launch",
"program": "${fileDirname}/${fileBasenameNoExtension}",
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": false,
"MIMode": "gdb",
"preLaunchTask": "compile",
"setupCommands": [
{
"description": "Enable pretty-printing for gdb",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
]
}
]
}
tasks.json:
{
"version": "2.0.0",
"tasks": [{
"label": "compile",
"command": "g++",
"args": [
"-g",
"${file}",
"-o",
"${fileDirname}/${fileBasenameNoExtension}"
],
"problemMatcher": {
"owner": "cpp",
"fileLocation": [
"relative",
"${workspaceRoot}"
],
"pattern": {
"regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",
"file": 1,
"line": 2,
"column": 3,
"severity": 4,
"message": 5
}
},
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
注意,如果是C语言需要将g++改为gcc。
到这里,我们已经完成了C++环境的配置。
ROS的初步学习
我按照古月居的21讲的顺序来学习ROS相关知识。课程链接如下:
一、ROS命令行工具
我们可以先运行小海龟程序,就是上面的三句代码,按table可以自动补全,当成功仿真后,我们再打开一个终端,执行下面语句:
rqt_graph
它是ROS自带的一种图形化工具,可以呈现节点之间的通讯关系。
常用的还有如下所示的代码,其后面要跟另外的语句,直接执行可以显示后面可以跟的语句以及具体功能:
rosnode
rostopic
rosmsg
rosservice
rosbag
这些也体现了ros当中的一些基本概念,即节点,话题,消息,服务等,我们放在下一节介绍,而rosbag为一个话题记录和复现工具,比如下面这样:
rosbag record -a -O cmd_record
rosbag play cmd_record.bag
在执行完第一句话后移动小海龟,其对应信息就会被记录在cmd_record.bag文件当中,此时再play执行该文件就能实现话题复现。
二、ROS基础知识
首先要知道ROS的通信机制是一个松散耦合的分布式软件框架设计而来的,它里面可以有很多节点(node),所谓节点,就是ROS当中的最小处理单元,它是一个执行单元,用来完成特定的功能,比如说一个节点完成图像识别,另一个完成图像驱动,他们之间会有数据传输。建议为每一个目标功能创建一个节点,将程序细分化方便扩展和使用,但要注意,在一个系统中节点的名字是唯一的。
不同的节点之间可以使用不同的编程语言,比如说可以一个节点用C++实现,另一个用Python来实现,最后通过ROS框架完成拼接。我们可以使用rosrun来命令运行一个功能包当中的节点(不需要知道包的路径)这点在后面会有实例。
下面介绍节点管理器(master),也会被叫做主节点(有点类似main函数的意思)。节点管理器负责节点到节点的连接和消息通信,类似于服务器。roscore是它的运行命令,当运行节点管理器时,你可以注册每个节点的名字,并根据需要获取信息。没有节点管理器,就不能在节点之间建立访问和消息交流(如话题和服务),节点管理器还提供一个参数服务器的功能,会记录全局变量的变量名和变量值,就可以供别的节点访问设置或读写。
前面我们说到节点之间的消息通信,其实,ROS给了节点之间三种核心通讯方式:话题(topic),服务(service),以及基于RPC的参数服务器。
话题就是ROS中一个数据传输的有名字的通道。当一个节点想要分享信息时,它就会发布(publish)消息到对应的一个或者多个话题;当一个节点想要接收信息时,它就会订阅(subscribe)它所需要的一个或者多个话题。 ROS节点管理器负责确保发布节点和订阅节点能找到对方;而且消息是直接地从发布节点传递到订阅节点,中间并不经过节点管理器转交。简单来说,话题通讯模型包含发布者(publisher)和订阅者(subscriber),其中,发布者先向节点管理器注册节点,然后告诉管理器要向哪个话题发布数据,最后在节点运行时采集数据,然后发布到这个话题;而订阅者也是先向节点管理器注册节点,然后订阅话题,告诉管理器要接受哪个话题的数据,最后在节点运行时就接收数据,然后处理数据(一般会有回调函数,类似中断服务)。
话题通讯是一种异步通讯,即发布者把数据发布之后,订阅者可能过很久之后才会接收。
话题通讯中这些数据叫做消息(message),它用来描述我们传输的话题数据,它可以是各种类型的数据,例如整形,浮点型等等,这个我们可以使用自带的消息,也可以自己定义,具体的实现在后面会给出。
service服务通讯机制是一种双向同步数据传输模式。基于客户端/服务器模型,两部分通信数据类型:一个用于请求,一个用于应答,类似web服务器。
服务端(server)启动后会通过RPC在ROS Master中注册自身信息,包含提供的服务名称, ROS Master 会将节点的注册信息加入到注册表中。客户端(client)启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要请求的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。ROS Master 会根据注册表中的信息匹配Server和 Client,并通过 RPC 向 Client 发送 Server 的TCP地址信息。Client 根据响应的信息,使用 TCP 与 Server 建立网络连接,并发送请求数据。Server 接收、解析请求的数据,并产生响应结果返回给 Client。这就是服务通讯的具体流程。
简单来说,话题通讯和服务通讯可以由下面这张表来对比观看:
比较项 | 话题 | 服务 |
同步性 | 异步 | 同步 |
通信模型 | 发布+订阅 | 客户端+服务器端 |
反馈机制 | 无 | 有 |
缓冲区 | 有 | 无 |
节点关系 | 多对多 | 一(server)对多 |
传输数据格式 | *.msg | *.srv |
适合场景 | 数据传输 | 逻辑处理 |
三、工作空间和功能包的创建
工作空间(workspace)是一个存放工程开发相关文件的文件夹,包含代码空间(source space),编译空间(build space),开发空间(development space)和安装空间(install space)。工作空间的创建包括如下几步:
mkdir -p ~/catkin_ws/src
cd ~/catkin_ws/src
catkin_init_workspace
这三句话的意思分别是:创建文件夹,工作空间名字为“catkin_ws”;切换路径到子文件夹下;初始化工作空间把文件夹属性切换为ROS工作空间属性。
cd ~/catkin_ws/
catkin_make
这两步进行编译,此时会看到工作空间里已经有三个文件夹了:devel,build以及我们创建的src。编译不会自己产生安装空间,需要我们用下面这条语句产生:
catkin_make install
编译完之后,要需要设置环境变量,下面两条语句分别是设置环境变量与检查环境变量:
source devel/setup.bash
echo $ROS_PACKAGE_PATH
当然,这只对当前终端生效,如果不想每次都重复地添加环境变量,可以输入以下命令:
echo "source ~/catkin_ws/devel/setup.bash" >> ~/.bashrc
输入完之后,记得检查一下哦。这个指令其实就是向setup.bash文件里写入设置环境变量这句话,当然也可以自行找到这个文件手动写入,如果找不到可以按ctrl+H显示隐藏的文件。
创建完工作空间之后我们就可以创建功能包了,同一个工作空间不能有同名的功能包,但同名的功能包可出现在不同的工作空间当中。代码如下所示({PKG_NAME}为功能包名字,自己命名,后面三个跟的是依赖):
cd ~/catkin_ws/src
catkin_create_pkg {PKG_NAME} std_msgs rospy roscpp
cd ~/catkin_ws
catkin_make
创建完功能包后,我们就可以进行代码编写了。
四、话题通讯
4.1 发布者的编程实现
了解了话题通讯机制后,不难理解,其实在小海龟程序当中按下方向键就是发送了一些消息给指定的话题,那么不通过方向键而是直接发送消息给话题,可以实现小海龟的运动吗?显然是可以的,下面就让我们进行尝试。
运行roscore,再启动海龟仿真程序,忘了的可以翻前面的,然后我们再打开一个新的终端,输入如下语句(按table键可以补全,按左右键调整光标位置):
rostopic pub /turtle1/cmd_vel geometry_msgs/Twist "linear:
x: 1.0
y: 0.0
z: 0.0
angular:
x: 0.0
y: 0.0
z: 0.0"
回车之后,我们会发现小海龟沿x轴动了一下,那这条语句的意思也就很清楚了:使用话题通讯,向话题"/turtle1/cmd_vel"发布一条消息,消息内容包含了线速度linear和角速度angular,但是小海龟只会动一下,也就是说只发布了一次消息,如果想让小海龟一直动,就需要以固定频率发送消息,只需在pub和/turtle1/cmd_vel之间加上频率-r N即可,其中N为频率,可以先输入5试一下。到这为止,我们就体验了ROS当中的话题通讯,那我们可不可以换一种形式,用C++代码实现发布消息的功能呢?毋容置疑,肯定是可以的,下面我们将动手实践一下。
在写发布者之前,需要提前声明一下,为了不重复讲解,这里先进行消息的编写,即我们不以小海龟程序自带的话题消息为例,而是自己动手写一个自定义的消息,然后再尝试写一个发布者将自定义的消息发布出去。
首先,在我们的功能包里(我这里功能包的名字叫learning_topic)打开终端,创建消息文件,如下所示:
mkdir msg
cd msg
touch Person_alone.msg
这样,我们就成功创建了一个msg文件夹以及文件夹下的Person_alone.msg文件,双击打开文件,输入如下内容:
string name
uint8 age
从内容不难看出,这是来描述一个人的姓名和年龄的数据结构,其中string表示字符串,uint8(注意不是unit)表示无符号8位整型,在定义了消息之后编写代码在使用时会根据具体编写语言自动适应。编写消息是不是非常简单,但要让ROS识别就没那么简单了。我们回到我们的功能包中,会发现有这样两个文件:CMakeLists.txt和package.xml。对,我们就是要修改这两个文件来让ROS知道这是一个自定义消息。
在package.xml中添加如下代码,注意添加的位置,放在<export>之前。如图所示。
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
在CMakeLists.txt中进行如下修改(请忽略我那个turtlesim和geometry_msgs):
find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
message_generation
)
add_message_files(
FILES
Person_alone.msg
)
generate_messages(
DEPENDENCIES
std_msgs
)
catkin_package(
# INCLUDE_DIRS include
# LIBRARIES beginner_tutorials
CATKIN_DEPENDS roscpp rospy std_msgs message_runtime
# DEPENDS system_lib
)
完成上述工作后编译整个工作空间,然后我们就可以在devel/include/learning_topic里面找到这样的文件:Person_alone.h。表明已经成功帮我们编译好了头文件:
有了自定义消息之后,我们尝试把它发布出去,需要编写publisher代码,如下所示:
#include<ros/ros.h>
//引用自定义消息类型
#include "learning_topic/Person_alone.h"
int main(int argc,char **argv)
{
//初始化节点,名字为“person_publisher”
ros::init(argc,argv,"person_publisher");
//创建节点句柄,可以理解为节点控制器
ros::NodeHandle n;
//创建名为person_pub的发布者,消息类型为learning_topic::Person_alone,话题名字为person_info,队列长度为100
ros::Publisher person_pub = n.advertise<learning_topic::Person_alone>("person_info",100);
//设置发送频率,单位为赫兹
ros::Rate loop_rate(1);
int count = 0;
while(ros::ok())
{
//创建自定义消息的类
learning_topic::Person_alone person_alone;
//赋值
person_alone.name = "Tom";
person_alone.age = 18;
//通过发布者person_pub发送消息person_alone
person_pub.publish(person_alone);
//在终端显示信息
ROS_INFO("Publish Person Info:name:%s age:%d",person_alone.name.c_str(),person_alone.age);
//阻塞函数,用来处理回调函数,该节点只是发布者并未订阅消息,故可有可无
ros::spinOnce();
//根据设置的频率休眠
loop_rate.sleep();
++count;
}
return 0;
}
编写完publisher之后,我们需要修改CMakeLists.txt(功能包里的那个)文件来让它变为可执行文件,在里面加上这样的话:
add_executable(person_pub src/person_pub.cpp)
target_link_libraries(person_pub ${catkin_LIBRARIES})
add_dependencies(person_pub ${PROJECT_NAME}_generate_messages_cpp)
之后就是熟悉的编译啦,编译完之后我们可以试着运行一下,记得先打开终端运行roscore哦,然后再打开一个终端,输入rosrun learning_topic person_pub即可,如果前面没有问题那么会看到下面的结果:
这就表明我们已经成功实现了自定义消息并且创建了一个发布者将消息成功发布了出去。
4.2 订阅者的编程实现
同样的,理解了发布者的代码之后,我们不难写出订阅者的代码。
#include<ros/ros.h>
//引用自定义消息类型
#include "learning_topic/Person_alone.h"
//回调函数
void personCallback(const learning_topic::Person_alone::ConstPtr& person_alone)
{
//在终端显示信息
ROS_INFO("hello [%s]",person_alone->name.c_str());
}
int main(int argc,char **argv)
{
//初始化节点,名字为“person_subscriber”
ros::init(argc,argv,"person_subscriber");
//创建节点句柄,可以理解为节点控制器
ros::NodeHandle n;
//创建名为person_sub的订阅者,订阅话题名字为person_info的消息,队列长度为100,注册回调函数personCallback
ros::Subscriber person_sub = n.subscribe("person_info",100,personCallback);
//阻塞函数
ros::spin();
return 0;
}
然后也是同样的修改CMakeLists.txt文件然后编译,下面我们来运行一下。
启动roscore之后输入rosrun learning_topic person_sub,会发现,咦?并没有任何结果,这是因为在话题person_info当中并没有消息发布,此时我们再运行前面的person_pub,就能看到下面的结果啦:
此时我们再打开终端输入rqt_graph就能看到下面的话题通讯关系图了,是不是非常清晰明了?
五、服务通讯
5.1 客户端的编程实现
有时候不需要发布者一直发布消息,而是我们需要了再用,这时就要使用服务通讯了,和前面话题通讯一样的思路,我们还是自定义消息类型,然后实现客户端和服务端。在功能包(我这里是learning_service)打开终端输入如下指令创建服务文件。(注意,如果使用和前面相同的功能包,就不要使用Person_alone这个名字了)
mkdir srv
cd srv
touch Person_alone.srv
同样的,双击打开srv文件输入以下内容:
string name
uint8 age
---
string result
我们来分析一下,上面的和之前定义的消息一样不用解释,关键在于下面的三个横杠,这是个分隔符,横杠上面的为请求(request),下面的为应答(response),这样是不是就感觉很好理解了?然后修改CMakeLists.txt和package.xml,和前面基本是一样的。
package.xml:
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
CMakeLists.txt:
find_package(catkin REQUIRED COMPONENTS
roscpp
rospy
std_msgs
message_generation
)
add_service_files(
FILES
Person_alone.srv
)
generate_messages(
DEPENDENCIES
std_msgs
)
catkin_package(
# INCLUDE_DIRS include
# LIBRARIES beginner_tutorials
CATKIN_DEPENDS roscpp rospy std_msgs message_runtime
# DEPENDS system_lib
)
还是请忽略我框出来的多余的部分,保存后在工作空间catkin_make一下,我们就能在devel/include/learning_service里面看到下面三个头文件了:
有小伙伴可能就要问了,为什么话题通讯里只有一个头文件而这个有三个,分析文件名不难理解,Person_alone.h是一个总的头文件,而Person_aloneRequest.h和Person_aloneResponse.h分别代表了请求和响应的头文件,在实际运用当中还是用第一个就好了。
下面是客户端的C++代码实现,注意写完之后记得修改编译文件哦,和前面一样的。
#include<ros/ros.h>
//引用自定义消息类型
#include "learning_service/Person_alone.h"
int main(int argc,char **argv)
{
//初始化节点,名字为“person_client”
ros::init(argc,argv,"person_client");
//创建节点句柄,可以理解为节点控制器
ros::NodeHandle n;
//发现show_person服务后,创建名为person_client的客户端,连接show_person服务
ros::service::waitForService("show_person");
ros::ServiceClient person_client = n.serviceClient<learning_service::Person_alone>("show_person");
//创建名为person_srv的服务,并赋请求值
learning_service::Person_alone person_srv;
person_srv.request.name = "Tom";
person_srv.request.age = 18;
//在终端显示信息
ROS_INFO("call service to show person[name:%s age:%d]",person_srv.request.name.c_str(),person_srv.request.age);
//请求服务调用
person_client.call(person_srv);
ROS_INFO("show person result:%s",person_srv.response.result.c_str());
return 0;
}
5.2 服务端的编程实现
服务端的代码如下所示:
#include<ros/ros.h>
//引用自定义消息类型
#include "learning_service/Person_alone.h"
//回调函数
bool personCallback(learning_service::Person_alone::Request &req,learning_service::Person_alone::Response &res)
{
//在终端显示信息
ROS_INFO("hello [%s]",req.name.c_str());
//设置反馈信息
res.result = "ok";
return true;
}
int main(int argc,char **argv)
{
//初始化节点,名字为“person_server”
ros::init(argc,argv,"person_server");
//创建节点句柄,可以理解为节点控制器
ros::NodeHandle n;
//创建名为person_server的服务端,提供的服务名为“show_person”,注册回调函数personCallback
ros::ServiceServer person_server = n.advertiseService("show_person",personCallback);
ROS_INFO("ready to show person information.");
//阻塞函数
ros::spin();
return 0;
}
在编译完成之后,我们来运行一下,还是那样,运行roscore,运行rosrun learning_service person_client,这时候终端会显示等待这个服务,这是因为我们还没启动服务端,好了,一样的输入rosrun learning_service person_server,会发现出现了下面这样的结果,证明是可以正常进行服务通讯的:
到这为止,就完成了ROS的通讯机制的理解了,希望这篇笔记能帮到您!
参考资料:
https://zhuanlan.zhihu.com/p/384578650