ROS基础,话题通信模式以及自定义话题消息编程案例服务通信模式,分布式通信
ROS基础,话题通信模式以及自定义话题消息编程案例、服务通信模式,分布式通信
目录
1.ROS通信机制
(1)节点(Node)-软件模块
(2)节点管理器( ROS Master)ーー控制中心,提供参数管理
(3)话题( Topic)一一异步通信机制,传输消息( Message)
(4)服务( Service)-ー同步通信机制,传输请求/应答数据
1.1 ROS通信模式–话题通信模型
流畅梳理:
Talker注册(又叫发布者,说话一方)
Listener注册(又叫订阅者,听话的一方)
ROS Master进行信息匹配
Listener发送连接请求
Talker确认连接请求
建立网络连接
Talkerl向 Listener发布数据
无论是Listener还是Talker,都不能直接进行通信,需由ROS Master代发。Listener向ROS Master询问(专业点称作订阅 subscribe)叫bar的话题(topic),若存在发布该话题的Talker,ROS Master响应给Listener,Listener与Talker建立TCP连接并通信。
备注 :
RPC是远程过程调用(Remote Procedure Call)的缩写形式,支持异构型分布式系统间的通讯
TCP是传输控制协议(Transmission Control Protocol)是一种面向连接的、可靠的、基于字节流的传输层通信协议。
实例:通过话题模式通信
下面通过一个小的实例来理解topic通信
1)创建工作空间与功能包
注意source的时候要看看当前的shell默认解释器是什么,通过echo $SHELL查看,我用的是zsh
$ source /opt/ros/kinetic/setup.zsh
$ mkdir -p ~/catkin_ws/src
$ cd ~/catkin_ws/src
$ catkin_init_workspace
$ cd ~/catkin_ws
$ catkin_make
$ souce ~/catkiin_ws/devel/setup.zsh
$ cd ~/catkin_ws/src
$ catkin_create_pkg test1 rospy roscpp
$ cd ~/catkin_ws
$ catkin_make
$ source devel/setup.zsh
2)创建 Listener 和 Talker
Listener
/**
* 该例程将订阅chatter话题,消息类型String
*/
#include "ros/ros.h"
#include "std_msgs/String.h"
// 接收到订阅的消息后,会进入消息回调函数
void chatterCallback(const std_msgs::String::ConstPtr& msg)
{
// 将接收到的消息打印出来
ROS_INFO("I heard: [%s]", msg->data.c_str());
}
int main(int argc, char **argv)
{
// 初始化ROS节点
ros::init(argc, argv, "listener");
// 创建节点句柄
ros::NodeHandle n;
// 创建一个Subscriber,订阅名为chatter的topic,注册回调函数chatterCallback
ros::Subscriber sub = n.subscribe("chatter", 1000, chatterCallback);
// 循环等待回调函数
ros::spin();
return 0;
}
节点句柄的作用:
1)自动地启动和关闭
在节点初始化和关闭一节中,使用ros::NodeHandle管理节点的内部引用,使启动和关闭一个节点变得简单.
在创建时,如果一个内部节点没有被启动,节点句柄将启动该节点.一旦所有的节点句柄实例被销毁,那么节点将被自动关闭.
2)名字空间
节点句柄允许给构造器明确一个名字空间
ros::NodeHandle nh(“my_namespace”);
这使节点拥有相对名字<node_namespace>/my_namespace,而不仅仅是<node_namespace>
也可以给节点句柄明确一个父节点句柄和一个紧跟的名字空间
ros::NodeHandle nh1(“ns1”);
ros::NodeHandle nh2(nh1, “ns2”);
这使节点句柄nh2进入<node_namespace>/nh1/nh2名字空间下
全局名字空间
ros::NodeHandle nh("/my_global_namespace");
ros::spin()的作用: 这篇文章讲的非常详细
ros::Subscriber: 用于订阅话题
Talker
/**
* 该例程将发布chatter话题,消息类型String
*/
#include <sstream>
#include "ros/ros.h"
#include "std_msgs/String.h"
int main(int argc, char **argv)
{
// ROS节点初始化
ros::init(argc, argv, "talker");
// 创建节点句柄
ros::NodeHandle n;
// 创建一个Publisher,发布名为chatter的topic,消息类型为std_msgs::String
ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter", 1000);
// 设置循环的频率
ros::Rate loop_rate(10);
int count = 0;
while (ros::ok())
{
// 初始化std_msgs::String类型的消息
std_msgs::String msg;
std::stringstream ss;
ss << "hello world " << count;
msg.data = ss.str();
// 发布消息
ROS_INFO("%s", msg.data.c_str());
chatter_pub.publish(msg);
// 循环等待回调函数
ros::spinOnce();
// 按照循环频率延时
loop_rate.sleep();
++count;
}
return 0;
}
ros::Publisher :发布话题
ros::Rate loop_rate(10): 发布频率,10HZ/s
ros::spinOnce(): 发布一次后等待订阅者接收,假设这里的发布频率为loop_rata(10),10hz/s,若是不定义消息池,发布一次要等待订阅者接收,这里肯定存在延时,如果网络比较慢就会造成比较严重的延时。那么就需要一个缓存池,先把消息丢缓存池里,让它持续不断的发布, 对于有些传输特别快的消息,尤其需要注意合理控制消息池大小和ros::spinOnce()执行频率; 比如消息送达频率为10Hz,接收方的ros::spinOnce()的回调频率为5Hz,那么消息池的大小就一定要大于2,才能保证数据不丢失,无延迟。这里的消息池1000,足够大了
3)添加到CmakeLists.txt
在ROS中但凡编写了程序文件都需要添加给CmakeLists.txt,否则编译时将找不到文件,在CmakeLists.txt末尾添加:
add_executable(talker src/talker.cpp)
target_link_libraries(talker ${catkin_LIBRARIES})
#add_dependencies(talker ${PROJECT_NAME}_generate_messages_cpp)
add_executable(listener src/listener.cpp)
target_link_libraries(listener ${catkin_LIBRARIES})
编译
$ catkin_make
不报错代表编译成功
4)执行
打开三个终端,第一个终端输入roscore,创建master进程,其余两个终端分别输入:
//终端1
$ roscore
//终端2
$ rosrun test1 listener
//终端3
$ rosrun test1 talker
注意:test1为我创建的功能包名,你要确认自己的功能包名叫什么
感受一下话题模式的效果:
1.1.1 自定义话题消息
1)创建消息文件
在功能包下创建msg文件夹并创建一个后缀为msg的消息文件
$ cd ~/catkin_make/src
$ mkdir msg
$ touch Person.msg
向Person.msg中添加:
ROS系统的变量类型是独立一档的,下面演示的是一些基础类型,如string,int类型。我们下面定义一个关于人的消息类型
string name
uint8 sex
uint8 age
uint8 unknown = 0
uint8 male = 1
uint8 female = 2
提示:uint8 unknown = 0 代表给一个整形的变量添加一个常量
2)在package.xml中添加功能包依赖
如果有该项就不要添加了
<build_depend>message_generation</build_depend>
<build_depend>std_msgs</build_depend>
<exec_depend>std_msgs</exec_depend>
<exec_depend>message_runtime</exec_depend>
3)在CMakeLists.txt中添加编译选项
找到find_package()在尾部添加,找到catkin_package在尾部添加
find_package( ... std_msgs message_generation)
catkin_package(CATKIN_DEPENDS ... std_msgs message_runtime)
add_message_files(FILES Person.msg)
generate_messages(DEPENDENCIES std_msgs)
4)编译以及source
之前也说了,但凡在ROS工作空间中做的修改都需要进行编译与source,谨记
$ catkin_make
$ source devel/setup.zsh
5)查看
可通过rosmsg查看该消息信息
$ rosmsg show Person
6)如何使用
要使用自定义的消息,需要导入消息头 #include “test1/Person.h”
通过工作空间名:消息文件名来创建消息对象,如:test1::Person p1
修改listener程序
/**
* 该例程将发布chatter话题,消息类型String
*/
#include <sstream>
#include "ros/ros.h"
#include "std_msgs/String.h"
#include "test1/Person.h"
int main(int argc, char **argv)
{
// ROS节点初始化
ros::init(argc, argv, "talker");
// 创建节点句柄
ros::NodeHandle n;
// 创建一个Publisher,发布名为chatter的topic,消息类型为std_msgs::String
//ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter", 1000);
ros::Publisher chatter_pub = n.advertise<test1::Person>("person", 1000);
// 设置循环的频率
ros::Rate loop_rate(10);
int count = 0;
while (ros::ok())
{
// 初始化std_msgs::String类型的消息
test1::Person p1;
p1.name="libai";
p1.sex=1;
p1.age=11;
// 发布消息
ROS_INFO("%s,%d,%d", p1.name.c_str(),p1.age,p1.sex);
chatter_pub.publish(p1);
// 循环等待回调函数
ros::spinOnce();
// 按照循环频率延时
loop_rate.sleep();
++count;
}
return 0;
}
修改talker程序
/**
* 该例程将订阅chatter话题,消息类型String
*/
#include "ros/ros.h"
#include "std_msgs/String.h"
#include "test1/Person.h"
// 接收到订阅的消息后,会进入消息回调函数
void chatterCallback(const test1::Person::ConstPtr& person)
{
// 将接收到的消息打印出来
ROS_INFO("I heard: [%s,%d,%d]", person->name.c_str(),person->age,person->sex);
}
int main(int argc, char **argv)
{
// 初始化ROS节点
ros::init(argc, argv, "listener");
// 创建节点句柄
ros::NodeHandle n;
// 创建一个Subscriber,订阅名为chatter的topic,注册回调函数chatterCallback
ros::Subscriber sub = n.subscribe("person", 1000, chatterCallback);
// 循环等待回调函数
ros::spin();
return 0;
}
改完记得编译并source,效果如下:
1.2 ROS通信模式–服务通信模型
Talker注册(又叫发布者,说话一方)
Listener注册(又叫订阅者,听话的一方)
ROS Master进行信息匹配
建立网络连接
Talkerl向 Listener发布服务应答数据
与话题通信模式不同的是,ROS Master在步骤2返回的是TCP/IP和端口号(话题通信模式返回的是RPC和端口号)
实例:通过服务模式进行通信
注意:本实例紧跟上面实例,所使用的工作空间和功能包名一致
1)实例说明
本实例创建两个程序文件,server.cpp与client.cpp,client传两个整数给server,server用来计算两个值的和。还需要创建一个自定义消息,与话题消息不同的是,服务消息以srv结尾
2)创建自定义消息
$ cd ~/catkin_ws/src
$ mkdir srv
$ cd srv
$ touch AddTwoInts.srv
int64 a b sum为三个64位的int整形,中间三个斜杆起到分隔作用,上面的部分作为输入参数,下面作为输出结果。
int64 a
int64 b
---
int64 sum
3)编写server.cpp 与 client.cpp
$ cd ~/catkin_ws/test1/src
$ touch server.cpp client.cpp
server.cpp
/**
* AddTwoInts Server
*/
#include "ros/ros.h"
#include "test1/AddTwoInts.h"
// service回调函数,输入参数req,输出参数res
bool add(test1::AddTwoInts::Request &req,
test1::AddTwoInts::Response &res)
{
// 将输入参数中的请求数据相加,结果放到应答变量中
res.sum = req.a + req.b;
ROS_INFO("request: x=%ld, y=%ld", (long int)req.a, (long int)req.b);
ROS_INFO("sending back response: [%ld]", (long int)res.sum);
return true;
}
int main(int argc, char **argv)
{
// ROS节点初始化
ros::init(argc, argv, "add_two_ints_server");
// 创建节点句柄
ros::NodeHandle n;
// 创建一个名为add_two_ints的server,注册回调函数add()
ros::ServiceServer service = n.advertiseService("add_two_ints", add);
// 循环等待回调函数
ROS_INFO("Ready to add two ints.");
ros::spin();
return 0;
}
#include “test1/AddTwoInts.h”: 导入自定义消息类型
test1::AddTwoInts::Request: 创建自定义消息类型的Request对象,格式为 功能包:自定义消息类型文件名:Request。
服务模式下的自定义消息类型有Request和Response对象,根据我们定义的类型,三斜杠上方的作为Request的参数,下方的作为Response
ros::ServiceServer service : 创建服务对象的server端对象
client.cpp
/**
* AddTwoInts Client
*/
#include <cstdlib>
#include "ros/ros.h"
#include "test1/AddTwoInts.h"
int main(int argc, char **argv)
{
// ROS节点初始化
ros::init(argc, argv, "add_two_ints_client");
// 从终端命令行获取两个加数
if (argc != 3)
{
ROS_INFO("usage: add_two_ints_client X Y");
return 1;
}
// 创建节点句柄
ros::NodeHandle n;
// 创建一个client,请求add_two_int service,service消息类型是test1::AddTwoInts
ros::ServiceClient client = n.serviceClient<test1::AddTwoInts>("add_two_ints");
// 创建test1::AddTwoInts类型的service消息
test1::AddTwoInts srv;
//atoll(argv[]) 接收参数
srv.request.a = atoll(argv[1]);
srv.request.b = atoll(argv[2]);
// 发布service请求,等待加法运算的应答结果
if (client.call(srv))
{
ROS_INFO("Sum: %ld", (long int)srv.response.sum);
}
else
{
ROS_ERROR("Failed to call service add_two_ints");
return 1;
}
return 0;
}
ros::ServiceClient client :创建服务类型的client端对象
client.call(srv): 通过call方法向请求服务端,传递自定义消息类型,请求成功访问1否则0
srv.response.sum: 输出response
4)修改CMakeLists.txt和Package.xml
在Package.xml填入
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
修改CMakeLists.txt
在find_package和catkin_package尾部加入消息类型的依赖,添加add_service_files()
再就是把两个写好的client.cpp和server.cpp添加进来。
add_dependencies(client ${PROJECT_NAME}_gencpp)由于是用c++写的,需要注入gencpp依赖,若是python就写genpy,但是要声明要项目名前缀,这些操作都是基础,注意一下格式即可。
find_package( ... message_generation)
catkin_package(CATKIN_DEPENDS ... message_runtime)
add_service_files(FILES AddTwoInts.srv)
add_executable(server src/server.cpp)
target_link_libraries(server ${catkin_LIBRARIES})
add_dependencies(client ${PROJECT_NAME}_gencpp)
add_executable(client src/client.cpp)
target_link_libraries(client ${catkin_LIBRARIES})
add_dependencies(client ${PROJECT_NAME}_gencpp)
5)编译
不报错代表编译成功
$ cd ~/catkin_ws
$ catkin_make
$ source ~/catkin_ws/devel/setup.zsh
6)执行
开三个终端,分别起roscore,rosrun server ,rosrun client
$ roscore
$ rosrun test1 server
$ rosrun test1 client 1 2
1.3 话题通信模型与服务通信模式的区别
在同步性上,话题模式采用异步通信,数据传输的可靠性比较低,若考虑高精度传输,对传输质量要求较高的话应该选择服务模式。
1.4 ROS通信机制-动作
动作(action)是一种带有带有连续反馈的通信机制,可在任务过程中止运行,基于ROS的消息机制实现。
Action的接口
- goal:发布任务目标
- cancel:请求取消任务
- status:通知客户端当前状态
- feedback:周期反馈任务运行的监控数据
- result:向客户端发送任务的执行结果(只发布一次)
实例:经典案例洗盘子
下面是一个洗盘子的案例。把洗盘子当做一个动作
goal对应洗盘子这个动作,result对应洗盘子的结果,feedback作为实时反馈,我们希望服务器(server)给我们洗盘子,client作为发起洗盘子动作的一方,当client发布一个goal,server收到后开始执行洗盘子动作,并实时反馈洗盘子的结果,直到希望后发布动作结束,result
1) action文件
action通信模式需要用到后缀名为.action的文件,分别定义goal目标,result结果和feedback反馈,以—分割
# Define the goal
uint32 dishwasher_id # Specify which dishwasher we want to use
---
# Define the result
uint32 total_dishes_cleaned
---
# Define a feedback message
float32 percent_complete
2 ) server
#include <ros/ros.h>
#include <actionlib/server/simple_action_server.h>
#include "test1/DoDishesAction.h"
typedef actionlib::SimpleActionServer<test1::DoDishesAction> Server;
// 收到action的goal后调用该回调函数
void execute(const test1::DoDishesGoalConstPtr& goal, Server* as)
{
ros::Rate r(1);
test1::DoDishesFeedback feedback;
ROS_INFO("Dishwasher %d is working.", goal->dishwasher_id);
// 假设洗盘子的进度,并且按照1hz的频率发布进度feedback
for(int i=1; i<=10; i++)
{
feedback.percent_complete = i * 10;
as->publishFeedback(feedback);
r.sleep();
}
// 当action完成后,向客户端返回结果
ROS_INFO("Dishwasher %d finish working.", goal->dishwasher_id);
as->setSucceeded();
}
int main(int argc, char** argv)
{
ros::init(argc, argv, "do_dishes_server");
ros::NodeHandle n;
// 定义一个服务器
Server server(n, "do_dishes", boost::bind(&execute, _1, &server), false);
// 服务器开始运行
server.start();
ros::spin();
return 0;
}
typedef actionlib::SimpleActionServer Server: 声明Server的类型为actionlib::SimpleActionServer ,typedef可为已有类型取一个新的名字,如声明typedef int myint后,可用通过myint 创建变量myint a =1;
const test1::DoDishesGoalConstPtr& goal: DoDishesGoalConstPtr是动作通信模式中的关于goal动作的封装 。 关于ConstPtr可看这篇文章
test1::DoDishesFeedback feedback: 当我们创建了DoDishes.action后,在编译时将生成DoDishesFeedback([ 0%] Built target _test1_generate_messages_check_deps_DoDishesActionFeedback),通过feedback可调用DoDishes.action定义的变量(Define a feedback message)
as->publishFeedback(feedback) :Server* as,as是个Server类对象,引用sever类中的publishFeedback方法,用于反馈当前状况
as->setSucceeded() :Server类中的成功反馈方法
Server server(n, “do_dishes”, boost::bind(&execute, _1, &server), false): 关于SimpleActionServer的详细使用方式参考官方文档 , 。在此我们使用的是是一种标准构造方式,第一个参数传句柄,第二个定义服务名(类似与话题名);第三传入一个回调方法,关于 boost::bind(&execute, _1, &server),调用boost:bind做函数,变量绑定,实际需要传入的参数是execute对象(回调上述定义的execute方法),execute传入两个参数goal和server,execute方法中的const test1::DoDishesGoalConstPtr& goal是一个常数(const),所以_1作为标识符(也叫占位符),无实际意义;第四个参数是否立即执行,不立即执行需要调用server.start()方法,
3)client
#include <actionlib/client/simple_action_client.h>
#include "test1/DoDishesAction.h"
typedef actionlib::SimpleActionClient<test1::DoDishesAction> Client;
// 当action完成后会调用该回调函数一次
void doneCb(const actionlib::SimpleClientGoalState& state,
const test1::DoDishesResultConstPtr& result)
{
ROS_INFO("Yay! The dishes are now clean");
ros::shutdown();
}
// 当action激活后会调用该回调函数一次
void activeCb()
{
ROS_INFO("Goal just went active");
}
// 收到feedback后调用该回调函数
void feedbackCb(const test1::DoDishesFeedbackConstPtr& feedback)
{
ROS_INFO(" percent_complete : %f ", feedback->percent_complete);
}
int main(int argc, char** argv)
{
ros::init(argc, argv, "do_dishes_client");
// 定义一个客户端
Client client("do_dishes", true);
// 等待服务器端
ROS_INFO("Waiting for action server to start.");
client.waitForServer();
ROS_INFO("Action server started, sending goal.");
// 创建一个action的goal
test1::DoDishesGoal goal;
goal.dishwasher_id = 1;
// 发送action的goal给服务器端,并且设置回调函数
client.sendGoal(goal, &doneCb, &activeCb, &feedbackCb);
ros::spin();
return 0;
}
client.sendGoal(goal, &doneCb, &activeCb, &feedbackCb): 这是client.sendGoal创建的标准模板(文档 )
&doneCb: 结束动作的Cb(callBack回调,下文同)
&activeCb: 开始动作的Cb
&feedbackCb: 实时反馈的Cb
具体如何去定义这些函数由使用者决定,你也可以什么也不写,仅创建这些函数并调用,实际的作用你可以下文的执行中看到
client.waitForServer() :客户端等待服务端响应
4)编译
编译前修改Package.xml和CMakeList.txt
Package.xml添加(动作通信模式必须导入的两个库):
<build_depend>actionlib</build_depend>
<build_depend>actionlib_msgs</build_depend>
<exec_depend>actionlib</exec_depend>
<exec_depend>actionlib_msgs</exec_depend>
CMakeList.txt添加
find_package(catkin REQUIRED actionlib_msgs actionlib)
add_action_files(DIRECTORY action FILES DoDishes.action)
generate_messages(DEPENDENCIES actionlib_msgs)
add_executable(server src/server.cpp)
target_link_libraries(server ${catkin_LIBRARIES})
add_dependencies(server ${PROJECT_NAME}_gencpp)
add_executable(client src/client.cpp)
target_link_libraries(client ${catkin_LIBRARIES})
add_dependencies(client ${PROJECT_NAME}_gencpp)
5)执行
开启三个终端,一roscore,二rosrun test1 DoDishes_server,三rosrun test1 DoDishes_client,实际执行效果对照程序进行观察
1.5 分布式通信
ROS是一种分布式软件框架,节点之间通过松耦合的方式进行组合。我们可以在本地配置机端的ip来实现对远程机端的控制
说白了ROS分布式通信机制就是在自己的电脑上操作ROS机端,而不需要ssh,因为ssh对图形支持不太友好,通过ROS提供的通信机制,你可以在本地调用rqt,rviz等工具,非常方便
操作方式:
1)设置IP地址,确保底层链路的连通
假设有两台装有ROS系统的ubuntu系统,U1与U2
U1 ip : 192.168.1.100
U2 ip:192.168.1.200
2)设置master主机
若U1作为机端(master),在U2端输入:
$ export ROS_MASTE_URI=http://192.168.1.100:11311
$ export ROS_HOSTNAME=192.168.1.200
ROS_MASTE_URI设置http协议访问机端,ROS_HOSTNAME设置本地访问的ip,同时声明了是谁对机端进行操作,声明这两条后就可以在本地调试机端。