Topic是ROS里一种异步通信的模型,一般是节点间分工明确,有的只负责发送,有的只负责接收处理。对于绝大多数的机器人应用场景,比如传感器数据收发,速度控制指令的收发,Topic模型是最适合的通信方式。
本文以MOOC《机器人操作系统入门》中的gps信号收发相关的代码作为例子。
事先建立topic_demo包,并在msg路径下创建gps.msg。即topic_demo/msg/gps.msg
string state #工作状态
float32 x #x坐标
float32 y #y坐标
创建完了msg文件,记得修改CMakeLists.txt和package.xml,从而让系统能够编译自定义消息。
CMakeLists.txt中需要的改动:
find_package(catkin REQUIRED COMPONENTS roscpp std_msgs
message_generation #需要添加这一句
)
add_message_files(FILES gps.msg)
#catkin在cmake之上新增的命令,指定从哪个消息文件生成
generate_messages(DEPENDENCIES std_msgs)
#catkin新增的命令,用于生成消息
#DEPENDENCIES后面指定生成msg需要依赖其他什么消息,由于gps.msg用到了flaot32这种ROS标准消息,因此需要再把std_msgs作为依赖
package.xml中需要添加的地方:
message_generation
message_runtime
若要在代码中使用自定义消息类型,只要#include
topic_demo::gps mygpsmsg;
mygpsmsg.x = 1.6;
mygpsmsg.y = 5.5;
mygpsmsg.state = "working";
新建 topic_demo/src/talker.cpp
#include
#include //自定义msg产生的头文件
int main(int argc, char **argv)
{
ros::init(argc, argv, "talker"); //用于解析ROS参数,第三个参数为本节点名
ros::NodeHandle nh; //实例化句柄,初始化node
topic_demo::gps msg; //自定义gps消息并初始化,msg为实例化的对象名称
...
ros::Publisher pub = nh.advertise("gps_info", 1); //创建publisher,往"gps_info"话题上发布消息. "gps_info"为定义的topic的名称
ros::Rate loop_rate(1.0); //定义发布的频率,1HZ
while (ros::ok()) //循环发布msg
{
... //处理msg
pub.publish(msg);//以1Hz的频率发布msg
loop_rate.sleep();//根据前面的定义的loop_rate,设置1s的暂停
}
return 0;
}
几个关键的地方:
1.创建publisher,并往"gps_info"话题上发布消息。
ros::Publisher pub = nh.advertise("gps_info", 1); //第一个参数"gps_info"定义了该topic的名称
2.实例化消息
topic_demo::gps msg; //自定义gps消息并初始化,msg为实例化的对象名称
创建 topic_demo/src/listener.cpp
#include
#include
#include
void gpsCallback(const topic_demo::gps::ConstPtr &msg)
{
std_msgs::Float32 distance; //计算离原点(0,0)的距离。std_msgs::Float32是ros中的一个结构体,类似于c++中的float distance
distance.data = sqrt(pow(msg->x,2)+pow(msg->y,2));
ROS_INFO("Listener: Distance to origin = %f, state: %s",distance.data,msg->state.c_str()); //输出
}
int main(int argc, char **argv)
{
ros::init(argc, argv, "listener");
ros::NodeHandle n;
ros::Subscriber sub = n.subscribe("gps_info", 1, gpsCallback); //设置回调函数gpsCallback
ros::spin(); //ros::spin()用于调用所有可触发的回调函数,将进入循环,不会返回,类似于在循环里反复调用spinOnce()
//而ros::spinOnce()只会去触发一次
return 0;
}
在topic接收方,有一个比较重要的概念,就是回调(CallBack),在本例中,回调就是预先给gps_info话题传来的消息准备一个回调函数,你事先定义好回调函数的操作,本例中是计算到原点的距离。只有当有消息来时,回调函数才会被触发执行。具体去触发的命令就是ros::spin(),它会反复的查看有没有消息来,如果有就会让回调函数去处理。
因此千万不要认为,只要指定了回调函数,系统就回去自动触发,你必须ros::spin()或者ros::spinOnce()才能真正使回调函数生效。
对于topic的接收方,有一个比较重要的概念——回调(CallBack),在本例中,回调就是预先给gps_info话题传来的消息准备一个回调函数,你事先定义好回调函数的操作。本例中是计算到原点的距离。
只有当有消息来时,回调函数才会被触发执行。具体去触发的命令就是ros::spin(),它会反复的查看有没有消息来,如果有就会让回调函数去处理。
add_executable(talker src/talker.cpp) #生成可执行文件talker
add_dependencies(talker topic_demo_generate_messages_cpp)
#表明在编译talker前,必须先生编译完成自定义消息
#必须添加add_dependencies,否则找不到自定义的msg产生的头文件
#表明在编译talker前,必须先生编译完成自定义消息
target_link_libraries(talker ${catkin_LIBRARIES}) #链接
add_executable(listener src/listener.cpp ) #声称可执行文件listener
add_dependencies(listener topic_demo_generate_messages_cpp)
target_link_libraries(listener ${catkin_LIBRARIES})#链接
完成以上步骤后,进行在ROS工作空间根目录进行catkin_make编译。
使用rosrun分别启动talker 和listener即可查看结果。
Service是一种请求-反馈的通信机制。请求的一方通常被称为客户端,提供服务的一方叫做服务器端。Service机制相比于Topic的不同之处在于:
创建service_demo/Greeting.srv文件,内容如下:
string name #短横线上边部分是服务请求的数据
int32 age
--- #短横线下面是服务回传的内容。
string feedback
srv格式的文件创建后,也需要修改CMakeLissts.txt,在其中加入
add_service_files(FILES Greeting.srv)
其余与添加msg的改动一样。
需要使用Greeting.srv文件时,只需要#include
service_demo::Greeting grt; //grt分为grt.request和grt.response两部分
grt.request.name = "HAN"; //不能用grt.name或者grt.age来访问
grt.request.age = "20";
创建service_demo/srv/server.cpp,内容如下:
#include
#include
bool handle_function(service_demo::Greeting::Request &req, service_demo::Greeting::Response &res){
//显示请求信息
ROS_INFO(“Request from %s with age %d”, req.name.c_str(), req.age);
//处理请求,结果写入response
res.feedback = “Hi ” + req.name + “. I’m server!”;
//返回true,正确处理了请求
return true;
}
int main(int argc, char** argv){
ros::init(argc, argv, “greetings_server”); //解析参数,命名节点
ros::NodeHandle nh; //创建句柄,实例化node
ros::ServiceServer service = nh.advertiseService(“greetings”, handle_function); //写明服务的处理函数
ros::spin();
return 0;
}
在以上代码中,服务的处理操作都写在处理函数handle_function()中,它的输入参数就是Greeting的Request和Response两部分,而非整个Greeting对象。
通常在处理函数中,我们对Requst数据进行需要的操作,将结果写入到Response中。
在roscpp中,handle_function()返回值是bool型,也就是服务是否成功执行。不要理解成输入Request,返回Response,在rospy中才是这样的。
创建service_demo/srv/client.cpp内容如下:
# include "ros/ros.h"
# include "service_demo/Greeting.h"
int main(int argc, char **argv)
{
ros::init(argc, argv, "greetings_client");// 初始化,节点命名为"greetings_client"
ros::NodeHandle nh;
ros::ServiceClient client = nh.serviceClient("greetings");
// 定义service客户端,service名字为“greetings”,service类型为Service_demo
// 实例化srv,设置其request消息的内容,这里request包含两个变量,name和age,见Greeting.srv
service_demo::Greeting srv;
srv.request.name = "HAN";
srv.request.age = 20;
if (client.call(srv))
{
// 注意我们的response部分中的内容只包含一个变量response,另,注意将其转变成字符串
ROS_INFO("Response from server: %s", srv.response.feedback.c_str());
}
else
{
ROS_ERROR("Failed to call service Service_demo");
return 1;
}
return 0;
}
以上代码比较关键的地方有两处:
第一个是建立client
nh.serviceClient("greetings") // 指明服务的类型和服务的名称
第二个是调用服务
client.call(srv) //注意返回结果不是response,而是是否成功调用远程服务。
…
CMakeLists.txt和pacakge.xml修改方法和topic_demo修改方法类似,不再赘述。
参考资料:
[1]: MOOC网《机器人操作系统入门》