ROS功能包及其应用

平台

  Ubuntu16.04
  ROS Kinetic Kame

环境配置

  具体详见该博客

创建工作空间Work Space

  首先创建ROS的工作空间,一般创建在Home目录下,使用命令行或者右击创建文件夹均可。例如,我在Home文件夹下创建一个名为cxx_ws(也可以是其他名字)的工作空间。
  然后创建src文件夹,并初始化。

1
2
3
mkdir src    
cd src
catkin_init_workspace

初始化工作空间

  然后回到工作空间,并编译。

1
2
cd ..
catkin_make

  编译完成后,工作空间会新增2个文件夹,builddevel。其中build文件夹为编译空间Build Spacedevel为开发空间Development Space
编译工作空间

  注:catkin_make含义:CMake(cross platform make)是一个跨平台的安装(编译)工具,可以用简单的语句来描述所有平台的安装(编译过程),能够输出各种各样的makefile或者project文件。而catkin_makecmake的升级版,可以认为是对cmake进一步封装的高级命令。
  附:ROS工程文件结构:
ROS文件夹结构

  添加环境变量

1
2
3
4
source devel/setup.bash  # 该文件定义了工作空间所需要的环境变量
gedit ~/.bashrc # .bashrc类似于windows下的环境变量
# source ~/cxx_ws/devel/setup.bash # 在打开的文件中添加该命令
source ~/.bashrc

添加环境变量

创建功能包package

  ROS功能包package指的是一种特定的文件结构和文件夹组成。工程文件夹下的src文件夹由一个个package功能包组成,一个功能包中可以包含一个或多个节点的文件。

创建package

  创建功能包的指令为:

1
catkin_create_pkg <package_name> [depend1] [depend2]

  package_name为包名,depend为依赖包名。
  进入src文件夹,创建hello功能包。

1
2
cd src
catkin_create_pkg hello std_msgs roscpp

  回到工作空间并编译:

1
2
cd ..
catkin_make

创建功能包
  注:同一个工作空间下,不允许存在同名功能包,不同工作空间下,允许存在同名功能包。功能包名字只能使用小写字母、数字和下划线,首字符必须是小写字母。
  std_msgs包含了常见消息类型。roscpp使用C++实现ROS的各种功能,提供了一个客户端库。
  创建完成的功能包一般会自带includesrcCMakeList.txtpackage.xml文件。

package详解

  通常,一个完整的package由以下几个部分组成:

  1. CMakeLists.txt:定义package的包名、依赖、源文件、目标文件等编译规则。
  2. package.xml:描述package的包名、版本号、作者、依赖等信息。
  3. src:存放ROS的源代码,包括.cpp.py
  4. include:存放C++源代码对应的头文件。
  5. scirpts:存放可执行脚本文件。
  6. msg:存放自定义格式的消息(.msg)。
  7. srv:存放自定义格式的服务(.srv)。
  8. models:存放机器人或仿真场景的3D模型(.sda,.stl,.dae)。
  9. urdf:存放机器人的模型描述(.urdf,.xacro)。
  10. launch:存放launch文件(.launch,.xml)。
    通常除了CMakeLists.txtpackage.xml是必须的,其他根据需求添加,实际建立工程文件时,都要遵守以上的命名规则。

package.xml

  package.xml包含了package的名称、版本号、内容描述、维护人员、软件许可、编译构建工具、编译依赖、运行依赖等信息。文件写法遵循XML标签文本的写法,目前存在两种格式,但内容大致一样。(红色字体的是必备标签)
package.xml参数

CMakeLists.txt

  CMakeList.txt原本是Cmake编译系统的规则文件,ROS的构建系统catkin基本上使用CMake,只是针对ROS工程添加了一些宏定义。因此在写法上CMakeList.txtCmake基本一致。CMakeList.txt文件规定了catkin的编译规则,直接规定这个包需要依赖那些package,要编译生成那些目标,如何编译等等流程写法。该文件的基本语法都是按照CMake,只是在其基础上添加了一些宏。
CMakeLists.txt参数

package编译过程

  功能包的编译过程也就是catkin的编译过程。

  1. 首先在工作空间的src目录下递归的查找每一个rospackage
  2. 因为每一个package中都有package.xmlCMakeList.txt文件,所以catkin编译系统依据CMakeList.txt文件,生成makefile文件,放在工作空间的build文件夹中。
  3. 然后make刚刚生成的makefiles等文件,编译链接生成可执行文件,放在工作空间下的devel文件夹中。
    简而言之,catkin就是将camkemake指令做了一个封装从而完成整个编译过程的工具。

创建ROS节点

  一个节点就是ROS下面一个可执行程序,使用ROS可以与其他节点进行通信。

编写Node

  Node文件通常放在package下的src文件夹中。
  例如在本例中,进入hello文件夹的src文件夹,右击创建一个cpp文件:hello_node.cpp
  打开hello_node.cpp文件,并输入代码:

1
2
3
4
5
6
7
8
#include <ros/ros.h>

int main (int argc, char **argv)
{
ros::init(argc, argv, "hello_node") ;
ros::NodeHandle nh;
ROS_INFO_STREAM("Hello, ROS!") ;
}

  #include <ros/ros.h>为声明ROS的标准库。
  ros::init(argc, argv, "hello_node")ROS初始化节点函数,其调用形式一般为:ros::init(argc, argv, "my_node_name");。本例中,将node_name取名为hello_node,当然也可以是其他名字。
  ros::NodeHandle nh;ROS启动节点函数。一个ROSnode只有一个NodeHandle,它提供这个node的对于topic的收发功能。
  ROS_INFO_STREAM("Hello, ROS!") ;打印日志信息,相对于print

编译Node

声明依赖库

  依赖库的声明在package.xml中,打开该文件,检查是否所有的依赖库都已安装。(通常,创建package时都会声明好的)

声明可执行文件

  打开CMakeLists.txt文件,找到注释掉的Declare a C++ executable声明C++可执行文件这一行,在这一段的最后,按照注释添加声明,在本例中,添加如下命令:(hello_node为可执行文件名字,可以换成其他的,下同)

1
add_executable(hello_node src/hello_node.cpp)

  再往下找到注释掉的Specify libraries to link a library or executable target aginst指定链接库这一行,在这一段的最后,按照注释添加声明,在本例中,添加如下指令:

1
target_link_libraries(hello_node ${catkin_LIBRARIES})

  修改完成后,回到工作空间文件夹,重新catkin_make编译即可。
  编译完成后,会在devel/lib/hello文件夹下生成hello_node可执行文件。
  注:这里的hello_node也就是后面rosrun命令中调用的节点名称,也就是CMakeLists文件中 add_executabletarget_link_libraries中的名字(这2个名字必须一致),但和hello_node.cpp文件中定义的节点名称可以不一致。

运行Node

  运行指令为:rosrun package-name executable-name,其中package-name为功能包名称,本例中为helloexecutable-name为可执行文件名称,即在上文声明可执行文件中的可执行文件名字,在本例中为hello_node,也可以在lib文件夹中找到可执行文件名字(可以反过来使用这个规则,即当程序报错找不到节点时,可以去lib文件夹看一下有没有生成的可执行文件)。
  Node节点不能单独单独运行,需要一个节点管理器才能正常运行。
  首先在终端输入roscore:启动节点管理器。
  然后再打开一个终端,输入运行节点命令:rosrun hello hello_node即可看到打印出来的信息。
启动节点

创建launch文件

  launch文件即启动文件,可以编写很多节点一起启动(rosrun一次只能启动一个节点),而且也会自动启动roscore命令,在大型工程中使用起来非常方便。

编写launch文件

  launch文件一般放在包文件下的launch文件夹下(如没有则新建),在本例中,在hello包文件夹下新建一个launch文件夹,然后进入该文件夹,新建hello_launch.launch文件。打开并输入以下内容:

1
2
3
<launch>
<node pkg="hello" type="hello_node" name="hello_launch" output="screen"/>
</launch>

创建launch文件
  pkgpackage name,即功能包名,本例中为hellotypeexecutable name,即指向节点的可执行文件的名称,本例中为hello_node。这两个参数也相当于rosrun命令中的2个参数。namenode name,即节点运行的名字,这里会覆盖节点定义中的init()的节点的名称。本例中定义为hello_launch,当然也可以是其他名字。

运行launch文件

  启动命令为:roslaunch package-name launch-name,其中package-name为功能包名称,本例中为hellolaunch-namelaunch文件名称(包括后缀),在本例中为hello_launch.launch
  打开终端输入rosluanch hello hello_launch.launch即可。
启动launch文件
  可以看到输出的日志信息。

launch文件解读

  launch文件中常用的参数为:
launch参数
  参考博客:解析 roslaunch 文件

ROS中的通讯

  ROS是以节点的形式开发的,而节点是根据其目的细分的可执行程序的最小单位。节点通过消息(message)与其他的节点交换数据,最终成为一个大型的程序。这里的关键概念是节点之间的消息通信,它分为三种。单向消息发送/接收方式的话题(topic);双向消息请求/响应方式的服务(service);双向消息目标(goal)/结果(result)/反馈(feedback)方式的动作(action)。
ROS消息通讯
ROS消息通讯

Topic in roscpp

  Topic(话题)是ROS里一种异步通信的模型,节点间分工明确,有的只负责发送,有的只负责接收处理。对于绝大多数的机器人应用场景,比如传感器数据收发,速度控制指令的收发,Topic模型是最适合的通信方式。ROS中的通信方式中,topic是常用的一种。对于实时性、周期性的消息,使用topic来传输是最佳的选择。topic是一种点对点的单向通信方式,这里的“点”指的是node,也就是说node之间可以通过topic方式来传递信息。
  topic要经历下面几步的初始化过程:首先,publisher节点和subscriber节点都要到节点管理器进行注册,然后publisher会发布topicsubscribermaster的指挥下会订阅该topic,从而建立起sub-pub之间的通信。注意整个过程是单向的。

创建Topic消息类型

  ros中自带很多种topic类型(可以在/ros/kinect/share中带_msg的文件夹中查看,例如查看标准消息类型,找到ros/kinect/share/std_msg/msg,里面定义了大量的topic类型数据。),也可以自己新建topic类型。例如在工程文件夹下的功能包hello下面,创建msg文件夹,并在新建hello_msg.msg

1
2
string state
int32 num

创建topic消息数据类型
  上述命令相对于创建了一个类似于C语言中的结构体,其中包含了string类型的state变量和int32类型的num变量。

编译工程

  打开该功能包下的CMakeLists.txt文件,找到fin_packageadd_message_filesgenerate_messages这几行,添加如下内容:
修改CMakeLists.txt

  打开该功能包下的package.xml文件,在里面添加以下内容:
修改package.xml

  回到工程文件夹目录,进行catkin_make编译,打开工程文件夹下的devel/include/hello文件夹,可以看到新生成的hello_msg.h文件,后续需要该消息类型只需在添加该头文件即可。

编写发送文件

  进入功能包下的src文件夹,创建hello_talker.cpp,其内容如下:

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
#include <ros/ros.h>
#include <hello/hello_msg.h> //devel文件夹下面的hello_msg.h文件

int main(int argc, char **argv)
{
ros::init(argc, argv, "hello_talker"); // 初始化节点,hello_talker为节点名称
ros::NodeHandle nh;

hello::hello_msg msg;

// 初始化消息值
msg.state = "working";
msg.num = 1;

ros::Publisher pub = nh.advertise<hello::hello_msg>("hello_info", 1); // 创建发送者

ros::Rate loop_rate(1); // 定义发布的频率,1HZ

while(ros::ok())
{
ROS_INFO("hello_talker: Hello %d", msg.num); // 打印消息

msg.num += 1; // 消息类型数据处理

pub.publish(msg); // 发布消息

loop_rate.sleep(); // 根据前面的定义的loop_rate,设置1s的暂停
}

return 0;
}

  其中最重要的函数为topic消息发布函数advertise(),其调用格式为:ros::Publisher pub = nh.advertise<[msg_type]>([msgName], [msgCountLimit])。在本例中,消息格式为自定义的hello_msg,消息名称为hello_info(也可以是其他名字)。1表示每次只发送一次。在本例中,发送者会一直以1Hz的频率循环+1发送信息。publish()函数会向所有订阅该节点的接收者发送消息。
  在ROS中,消息有组织地存放在话题里。topic消息传递的理念是:当一个节点想要分享信息时,它就会发布(publish)消息到对应的一个或者多个话题;当一个节点想要接收信息时,它就会订阅(subscribe)它所需要的一个或者多个话题。ROS节点管理器负责确保发布节点和订阅节点能找到对方;而且消息是直接地从发布节点传递到订阅节点,中间并不经过节点管理器转交。

编写接收文件

  进入功能包下的src文件夹,创建hello_listener.cpp,其内容如下:

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
#include <ros/ros.h>
#include <hello/hello_msg.h> // 添加消息头文件
#include <std_msgs/Int32.h> // 添加标准消息的int32头文件

void helloCallback(const hello::hello_msg::ConstPtr &msg)
{
std_msgs::Int32 helloNum; // 定义int32消息类型变量
helloNum.data = msg->num + 1; // 注意这里只能是指针引用方式

ROS_INFO("listener: hello %d, state = %s", helloNum.data, msg->state.c_str()); // 打印信息
}

int main(int argc, char ** argv)
{
ros::init(argc, argv, "hello_listener"); // 初始化节点,hello_listener为节点名称

ros::NodeHandle nh;

ros::Subscriber sub = nh.subscribe("hello_info", 1, helloCallback); // 创建接收者

ros::spin(); // ros::spin()用于调用所有可触发的回调函数,将进入循环,不会返回,类似于在循环里反复
// 调用spinOnce()只会发送一次

return 0;
}

  其中最重要的函数为topic消息接收函数subscribe(),其调用格式为:ros::Subscriber sub = node_handle.subscribe(topic_name,queue_size, pointer_to_callback_function);。前2个参数和advertise()的参数一致,第三个参数为回调函数的指针(这里是指针,所以只需写函数名即可,不需要加小括号())。当接收者接收到消息时,则会进入到回调函数中处理数据。本例中,接收者会将接收到的消息数值+1并打印输出状态信息。

编译运行

  打开功能包下的CMakeLists.txt文件,修改内容如下:
修改CMakeLists
  进入工程文件夹下,使用catkin_make编译。编译完成后,可以进入devel/lib/hello文件夹下面看到新生成的hello_talkerhello_listener可执行文件。
  首先打开节点管理器roscore,然后分别打开2个终端,运行指令:

1
rosrun hello hello_talker

1
rosrun hello hello_listener

运行topic节点
  此时可以再打开1个终端,并输入rqt_graph查看当前的节点及消息。
节点查看器
  由图可见,当前共有2个节点和1个消息(椭圆为节点,矩形为消息),即hello_talkerhello_listener通过消息(话题)hello_info来实现通讯。

Service in roscpp

  Service是一种请求-反馈的通信机制。请求的一方通常被称为客户端client,提供服务的一方叫做服务器端 serverService机制相比于Topic的不同之处在于:

  1. 消息的传输是双向的,有反馈的,而不是单一的流向。
  2. 消息往往不会以固定频率传输,不连续,而是在需要时才会向服务器发起请求。

创建Service消息类型

  和topci消息类型类似,ros也自带很多service消息类型,存放位置和topic一致,只是有的_msg类型不一定有service消息类型的数据。例如std_msg中就没有service类型的,而sensor_msg中存在srv文件夹,里面就有定义好的service消息类型数据。
  同样的,本例中也创建一个自己的service消息类型。在工程文件夹下的hello包中创建一个srv文件夹,新建hello_srv.srv

1
2
3
4
string name  
int32 age
---
string feedback

创建service消息数据类型
  横线上面的部分为服务请求的数据,即client数据,横向下面是服务器回传的内容,即server数据。类似于topic数据类型,service数据包含了2个结构体数据。

编译工程

  打开该功能包下的CMakeLists.txt文件,找到add_service_files这一行,根据注释添加以下内容:
修改CMakeLists.txt

  该部分内容一定要在generate_messages这一行之上,否则会编译报错。
  package.xml文件的添加内容如topic部分一致(如果在topic部分做过了就不需要再添加了)。
  回到工程文件夹目录,进行catkin_make编译,打开工程文件夹下的devel/include/hello文件夹,可以看到新生成的hello_srvRequest.hhello_srvResponse.h文件,其中Request.hclient的,Response.hserver的。

编写服务器端文件

  进入功能包下的src文件夹,创建hello_server.cpp,其内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <ros/ros.h>
#include <hello/hello_srv.h>

bool handle_function(hello::hello_srv::Request &req, hello::hello_srv::Response &res)
{
ROS_INFO("Request from client %s with age %d", req.name.c_str(),req.age);
res.feedback = "Hi " + req.name + ". I'm server!";
return true;
}

int main(int argc, char **argv){
ros::init(argc, argv, "hello_server");
ros::NodeHandle nh;
ros::ServiceServer service = nh.advertiseService("hello_server", handle_function);

ros::spin();
return 0;
}

编写客户端文件

  进入功能包下的src文件夹,创建hello_client.cpp,其内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <ros/ros.h>
#include <hello/hello_srv.h>

int main(int argc, char **argv){
ros::init(argc, argv, "hello_client");
ros::NodeHandle nh;
ros::ServiceClient client = nh.serviceClient<hello::hello_srv>("hello_server");

hello::hello_srv srv;
srv.request.name = "Cxx";
srv.request.age = 18;

if(client.call(srv))
{
ROS_INFO("Response from service: %s", srv.response.feedback.c_str());
}
else
{
ROS_ERROR("Failed to call service hello_service");
return 1;
}
return 0;
}

编译运行

  打开功能包下的CMakeLists.txt文件,修改内容如下:
修改CMakeLists.txt

  进入工程文件夹下,使用catkin_make编译。编译完成后,可以进入devel/lib/hello文件夹下面看到新生成的hello_serverhello_client可执行文件。
  首先打开节点管理器roscore,然后分别打开2个终端,运行指令:

1
rosrun hello hello_server

1
rosrun hello hello_client

  要先打开server节点程序,然后再打开client节点。
运行service节点
  service消息只会在client开启时才会调用,且调用完后立即结束,而topic会一直发送消息。

个人理解

  1. rosnode是最基本也是最小的一个单元,一个node就是一个main()函数,也就是一个可执行文件,而每个package则是由若干个node组成的一个功能包,最后一个个package组成了整个工程文件。因此,实际的工程项目最基础的还是基本功能的编写,ros系统只是一个上层的封装,可以更便于操纵传感器等硬件,是硬件和软件之间的一层系统,所以叫做机器人操作系统,但其本身又不像windows或者Ubuntu那样强大,只是针对机器人硬件进行了封装,所以还必须要依附在Ubuntu上面(据说现在也可以在windows上面安装了,具体还没有试过)。
  2. 创建完任何一个ROS工程空间,首先要做2件事情,1.编译工作空间;2.添加环境。环境变量添加一次即可,每次修改包里面的内容时都要重新回到工作空间编译,使其生效。编译工作空间的本质就是生成一些库文件、可执行文件,而其工具就是CMake这个交叉编译工具,也就是说通过CMake这个工具(在ros里面就是输入catkin_make),根据一定的规则和说明文件(在ros里面就是CMakeLists.txt文件),将package里面的文件转换为其他文件,例如将src里面的.cpp文件转为.exe文件(因为计算机最终执行的是二进制文件,也就是.exe文件,而.cpp文件属于高级语言,是人看的),将msg里面的文件转为lib里面的文件,等等。在通俗的讲,就是将我们看的东西转换为计算机看的东西。
  3. 个人觉得ros中有2个概念非常重要,一个是消息通讯,一个是launch文件,其余的概念接触过C++或者python都可以很快的理解。

参考博客

ROS—catkin编译系统、package.xml和CMakeList.txt文件
ros系统入门笔记(一)
ROS学习笔记三:编写第一个ROS节点程序
ROS 中的 launch 文件
ROS环境下launch文件格式说明
ROS Topic in roscpp 通信(简介+实例+测试)
ROS入门
机器人操作系统——Robot Operating System(ROS)

谢谢老板!
-------------本文结束感谢您的阅读给个五星好评吧~~-------------