ROS基础,话题通信模式以及自定义话题消息编程案例、服务通信模式,分布式通信

目录

1.ROS通信机制

1.1 ROS通信模式--话题通信模型

实例:通过话题模式通信

1.1.1 自定义话题消息

1.2 ROS通信模式--服务通信模型

实例:通过服务模式进行通信

1.3 话题通信模型与服务通信模式的区别

1.4 ROS通信机制-动作

实例:经典案例洗盘子

1.5 分布式通信

 

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()的作用:这篇文章讲的非常详细 https://www.cnblogs.com/liu-fa/p/5925381.html

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<test1::DoDishesAction> Server:声明Server的类型为actionlib::SimpleActionServer<test1::DoDishesAction> ,typedef可为已有类型取一个新的名字,如声明typedef int myint后,可用通过myint 创建变量myint a =1;

const test1::DoDishesGoalConstPtr& goal:DoDishesGoalConstPtr是动作通信模式中的关于goal动作的封装关于ConstPtr可看这篇文章https://blog.csdn.net/kantswang/article/details/82947669

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的详细使用方式参考官方文档http://docs.ros.org/diamondback/api/actionlib/html/classactionlib_1_1SimpleActionServer.html。在此我们使用的是是一种标准构造方式,第一个参数传句柄,第二个定义服务名(类似与话题名);第三传入一个回调方法,关于 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创建的标准模板(文档http://docs.ros.org/jade/api/actionlib/html/classactionlib_1_1SimpleActionClient.html#add37ef9123bfa8e3aca818e725e40c3c

&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,同时声明了是谁对机端进行操作,声明这两条后就可以在本地调试机端。

 

 

  • 7
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值