平台
Ubuntu16.04
ROS Kinetic Kame
环境配置
具体详见该博客
创建工作空间Work Space
首先创建ROS
的工作空间,一般创建在Home
目录下,使用命令行或者右击创建文件夹均可。例如,我在Home
文件夹下创建一个名为cxx_ws
(也可以是其他名字)的工作空间。
然后创建src
文件夹,并初始化。1
2
3mkdir src
cd src
catkin_init_workspace
然后回到工作空间,并编译。1
2cd ..
catkin_make
编译完成后,工作空间会新增2个文件夹,build
和devel
。其中build
文件夹为编译空间Build Space
,devel
为开发空间Development Space
。
注:catkin_make
含义:CMake
(cross platform make
)是一个跨平台的安装(编译)工具,可以用简单的语句来描述所有平台的安装(编译过程),能够输出各种各样的makefile
或者project
文件。而catkin_make
是cmake
的升级版,可以认为是对cmake
进一步封装的高级命令。
附:ROS
工程文件结构:
添加环境变量1
2
3
4source 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
2cd src
catkin_create_pkg hello std_msgs roscpp
回到工作空间并编译:1
2cd ..
catkin_make
注:同一个工作空间下,不允许存在同名功能包,不同工作空间下,允许存在同名功能包。功能包名字只能使用小写字母、数字和下划线,首字符必须是小写字母。
std_msgs
包含了常见消息类型。roscpp
使用C++实现ROS
的各种功能,提供了一个客户端库。
创建完成的功能包一般会自带include
、src
、CMakeList.txt
和package.xml
文件。
package详解
通常,一个完整的package
由以下几个部分组成:
- CMakeLists.txt:定义
package
的包名、依赖、源文件、目标文件等编译规则。 - package.xml:描述
package
的包名、版本号、作者、依赖等信息。 - src:存放
ROS
的源代码,包括.cpp
和.py
。 - include:存放
C++
源代码对应的头文件。 - scirpts:存放可执行脚本文件。
- msg:存放自定义格式的消息(.msg)。
- srv:存放自定义格式的服务(.srv)。
- models:存放机器人或仿真场景的3D模型(.sda,.stl,.dae)。
- urdf:存放机器人的模型描述(.urdf,.xacro)。
- launch:存放launch文件(.launch,.xml)。
通常除了CMakeLists.txt
和package.xml
是必须的,其他根据需求添加,实际建立工程文件时,都要遵守以上的命名规则。
package.xml
package.xml
包含了package
的名称、版本号、内容描述、维护人员、软件许可、编译构建工具、编译依赖、运行依赖等信息。文件写法遵循XML
标签文本的写法,目前存在两种格式,但内容大致一样。(红色字体的是必备标签)
CMakeLists.txt
CMakeList.txt
原本是Cmake
编译系统的规则文件,ROS
的构建系统catkin
基本上使用CMake
,只是针对ROS
工程添加了一些宏定义。因此在写法上CMakeList.txt
和Cmake
基本一致。CMakeList.txt
文件规定了catkin
的编译规则,直接规定这个包需要依赖那些package
,要编译生成那些目标,如何编译等等流程写法。该文件的基本语法都是按照CMake
,只是在其基础上添加了一些宏。
package编译过程
功能包的编译过程也就是catkin
的编译过程。
- 首先在工作空间的
src
目录下递归的查找每一个ros
的package
。 - 因为每一个
package
中都有package.xml
和CMakeList.txt
文件,所以catkin
编译系统依据CMakeList.txt
文件,生成makefile
文件,放在工作空间的build
文件夹中。 - 然后
make
刚刚生成的makefiles
等文件,编译链接生成可执行文件,放在工作空间下的devel
文件夹中。
简而言之,catkin
就是将camke
和make
指令做了一个封装从而完成整个编译过程的工具。
创建ROS节点
一个节点就是ROS
下面一个可执行程序,使用ROS
可以与其他节点进行通信。
编写Node
Node
文件通常放在package
下的src
文件夹中。
例如在本例中,进入hello
文件夹的src
文件夹,右击创建一个cpp
文件:hello_node.cpp
。
打开hello_node.cpp
文件,并输入代码:1
2
3
4
5
6
7
8
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
启动节点函数。一个ROS
的node
只有一个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_executable
和target_link_libraries
中的名字(这2个名字必须一致),但和hello_node.cpp
文件中定义的节点名称可以不一致。
运行Node
运行指令为:rosrun package-name executable-name
,其中package-name
为功能包名称,本例中为hello
,executable-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>
pkg
为package name
,即功能包名,本例中为hello
,type
为executable name
,即指向节点的可执行文件的名称,本例中为hello_node
。这两个参数也相当于rosrun
命令中的2
个参数。name
为node name
,即节点运行的名字,这里会覆盖节点定义中的init()
的节点的名称。本例中定义为hello_launch
,当然也可以是其他名字。
运行launch文件
启动命令为:roslaunch package-name launch-name
,其中package-name
为功能包名称,本例中为hello
,launch-name
为launch
文件名称(包括后缀),在本例中为hello_launch.launch
。
打开终端输入rosluanch hello hello_launch.launch
即可。
可以看到输出的日志信息。
launch文件解读
launch
文件中常用的参数为:
参考博客:解析 roslaunch 文件
ROS中的通讯
ROS
是以节点的形式开发的,而节点是根据其目的细分的可执行程序的最小单位。节点通过消息(message
)与其他的节点交换数据,最终成为一个大型的程序。这里的关键概念是节点之间的消息通信,它分为三种。单向消息发送/接收方式的话题(topic
);双向消息请求/响应方式的服务(service
);双向消息目标(goal
)/结果(result
)/反馈(feedback
)方式的动作(action
)。
Topic in roscpp
Topic
(话题)是ROS
里一种异步通信的模型,节点间分工明确,有的只负责发送,有的只负责接收处理。对于绝大多数的机器人应用场景,比如传感器数据收发,速度控制指令的收发,Topic
模型是最适合的通信方式。ROS
中的通信方式中,topic
是常用的一种。对于实时性、周期性的消息,使用topic
来传输是最佳的选择。topic
是一种点对点的单向通信方式,这里的“点”指的是node
,也就是说node
之间可以通过topic
方式来传递信息。
topic
要经历下面几步的初始化过程:首先,publisher
节点和subscriber
节点都要到节点管理器进行注册,然后publisher
会发布topic
,subscriber
在master
的指挥下会订阅该topic
,从而建立起sub-pub
之间的通信。注意整个过程是单向的。
创建Topic消息类型
ros
中自带很多种topic
类型(可以在/ros/kinect/share
中带_msg
的文件夹中查看,例如查看标准消息类型,找到ros/kinect/share/std_msg/msg
,里面定义了大量的topic
类型数据。),也可以自己新建topic
类型。例如在工程文件夹下的功能包hello
下面,创建msg
文件夹,并在新建hello_msg.msg
:1
2string state
int32 num
上述命令相对于创建了一个类似于C语言中的结构体,其中包含了string
类型的state
变量和int32
类型的num
变量。
编译工程
打开该功能包下的CMakeLists.txt
文件,找到fin_package
,add_message_files
和generate_messages
这几行,添加如下内容:
打开该功能包下的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
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
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
文件,修改内容如下:
进入工程文件夹下,使用catkin_make
编译。编译完成后,可以进入devel/lib/hello
文件夹下面看到新生成的hello_talker
和hello_listener
可执行文件。
首先打开节点管理器roscore
,然后分别打开2个终端,运行指令:1
rosrun hello hello_talker
1 | rosrun hello hello_listener |
此时可以再打开1个终端,并输入rqt_graph
查看当前的节点及消息。
由图可见,当前共有2个节点和1个消息(椭圆为节点,矩形为消息),即hello_talker
和hello_listener
通过消息(话题)hello_info
来实现通讯。
Service in roscpp
Service
是一种请求-反馈的通信机制。请求的一方通常被称为客户端client
,提供服务的一方叫做服务器端 server
。Service
机制相比于Topic
的不同之处在于:
- 消息的传输是双向的,有反馈的,而不是单一的流向。
- 消息往往不会以固定频率传输,不连续,而是在需要时才会向服务器发起请求。
创建Service消息类型
和topci
消息类型类似,ros
也自带很多service
消息类型,存放位置和topic
一致,只是有的_msg
类型不一定有service
消息类型的数据。例如std_msg
中就没有service
类型的,而sensor_msg
中存在srv
文件夹,里面就有定义好的service
消息类型数据。
同样的,本例中也创建一个自己的service
消息类型。在工程文件夹下的hello
包中创建一个srv
文件夹,新建hello_srv.srv
:1
2
3
4string name
int32 age
---
string feedback
横线上面的部分为服务请求的数据,即client
数据,横向下面是服务器回传的内容,即server
数据。类似于topic
数据类型,service
数据包含了2个结构体数据。
编译工程
打开该功能包下的CMakeLists.txt
文件,找到add_service_files
这一行,根据注释添加以下内容:
该部分内容一定要在generate_messages
这一行之上,否则会编译报错。
package.xml
文件的添加内容如topic
部分一致(如果在topic
部分做过了就不需要再添加了)。
回到工程文件夹目录,进行catkin_make
编译,打开工程文件夹下的devel/include/hello
文件夹,可以看到新生成的hello_srvRequest.h
和hello_srvResponse.h
文件,其中Request.h
为client
的,Response.h
为server
的。
编写服务器端文件
进入功能包下的src
文件夹,创建hello_server.cpp
,其内容如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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
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
文件,修改内容如下:
进入工程文件夹下,使用catkin_make
编译。编译完成后,可以进入devel/lib/hello
文件夹下面看到新生成的hello_server
和hello_client
可执行文件。
首先打开节点管理器roscore
,然后分别打开2个终端,运行指令:1
rosrun hello hello_server
1 | rosrun hello hello_client |
要先打开server
节点程序,然后再打开client
节点。
service
消息只会在client
开启时才会调用,且调用完后立即结束,而topic
会一直发送消息。
个人理解
ros
中node
是最基本也是最小的一个单元,一个node
就是一个main()
函数,也就是一个可执行文件,而每个package
则是由若干个node
组成的一个功能包,最后一个个package
组成了整个工程文件。因此,实际的工程项目最基础的还是基本功能的编写,ros
系统只是一个上层的封装,可以更便于操纵传感器等硬件,是硬件和软件之间的一层系统,所以叫做机器人操作系统,但其本身又不像windows
或者Ubuntu
那样强大,只是针对机器人硬件进行了封装,所以还必须要依附在Ubuntu
上面(据说现在也可以在windows
上面安装了,具体还没有试过)。- 创建完任何一个
ROS
工程空间,首先要做2件事情,1.编译工作空间;2.添加环境。环境变量添加一次即可,每次修改包里面的内容时都要重新回到工作空间编译,使其生效。编译工作空间的本质就是生成一些库文件、可执行文件,而其工具就是CMake
这个交叉编译工具,也就是说通过CMake
这个工具(在ros
里面就是输入catkin_make
),根据一定的规则和说明文件(在ros
里面就是CMakeLists.txt
文件),将package
里面的文件转换为其他文件,例如将src
里面的.cpp
文件转为.exe
文件(因为计算机最终执行的是二进制文件,也就是.exe
文件,而.cpp
文件属于高级语言,是人看的),将msg
里面的文件转为lib
里面的文件,等等。在通俗的讲,就是将我们看的东西转换为计算机看的东西。 - 个人觉得
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)