2021.3.14| ROS通信机制

今天学习了ROS的通信机制,目前把话题通信和服务通信过了一遍,把代码都跑了个遍,大体上掌握了一些规则。后面的参数服务器打算明天再看,先整理下目前所学的。

话题通信是ROS中使用频率最高的一种通信模式,话题通信是基于发布订阅模式的,也即:一个节点发布消息,另一个节点订阅该消息。

一、话题通信

话题通信实现模型是比较复杂的,该模型如下图所示,该模型中涉及到三个角色:

  • ROS Master (管理者)
  • Talker (发布者)
  • Listener (订阅者)

ROS Master 负责保管 Talker 和 Listener 注册的信息,并匹配话题相同的 Talker 与 Listener,帮助 Talker 与 Listener 建立连接,连接建立后,Talker 可以发布消息,且发布的消息会被 Listener 订阅。

 

编写话题通信的自定义msg类型的步骤一般如下:

  1. 自定义msg类型。在功能包下新建msg目录,添加文件xxx.msg,如Person.msg,如下。
    string name
    uint16 age
    float64 height
    
  2. 在package.xml中添加编译依赖和执行依赖。
      <build_depend>message_generation</build_depend>
      <exec_depend>message_runtime</exec_depend>
    
  3. CMakeLists.txt编辑msg相关配置。
    find_package(catkin REQUIRED COMPONENTS
      roscpp
      rospy
      std_msgs
      message_generation
    )
    # 需要加入 message_generation,必须有 std_msgs
    ## 配置 msg 源文件
    add_message_files(
      FILES
      Person.msg
    )
    # 生成消息时依赖于 std_msgs
    generate_messages(
      DEPENDENCIES
      std_msgs
    )
    
    #执行时依赖
    catkin_package(
    #  INCLUDE_DIRS include
    #  LIBRARIES demo02_talker_listener
      CATKIN_DEPENDS roscpp rospy std_msgs message_runtime
    #  DEPENDS system_lib
    )
    
  4. 编译后,就会发现devel/include/包名/xxx.h。
  5. 将上述头文件的所在路径写入c_pp_properties.json中的includePath段中,如下。
    "/home/xxx/包名/devel/include/**"
    

编写并实现话题通信的步骤如下:

1.编写发布方publisher

  • _ws/src/包名/src/下新建cpp文件。

  • 包含头文件,如

    #include "ros/ros.h"
    
  • 初始化ROS节点。

    ros::init(argc,argv,"");
    
  • 实例化ROS句柄。

    ros::NodeHandle nh;
    
  • 实例化发布者对象。

    ros::Publisher pub = nh.advertise<std_msgs::String>("",10);
    
  • 组织被发布的数据,并编写逻辑发布数据

  完整代码如下:

// 1.包含头文件 
#include "ros/ros.h"
#include "std_msgs/String.h" //普通文本类型的消息
#include <sstream>

int main(int argc, char  *argv[])
{   
    //设置编码
    setlocale(LC_ALL,"");

    //2.初始化 ROS 节点:命名(唯一)
    // 参数1和参数2 后期为节点传值会使用
    // 参数3 是节点名称,是一个标识符,需要保证运行后,在 ROS 网络拓扑中唯一
    ros::init(argc,argv,"talker");
    //3.实例化 ROS 句柄
    ros::NodeHandle nh;//该类封装了 ROS 中的一些常用功能

    //4.实例化 发布者 对象
    //泛型: 发布的消息类型
    //参数1: 要发布到的话题
    //参数2: 队列中最大保存的消息数,超出此阀值时,先进的先销毁(时间早的先销毁)
    ros::Publisher pub = nh.advertise<std_msgs::String>("chatter",10);

    //5.组织被发布的数据,并编写逻辑发布数据
    //数据(动态组织)
    std_msgs::String msg;
    // msg.data = "你好啊!!!";
    std::string msg_front = "Hello 你好!"; //消息前缀
    int count = 0; //消息计数器

    //逻辑(一秒10次)
    ros::Rate r(1);

    //节点不死
    while (ros::ok())
    {
        //使用 stringstream 拼接字符串与编号
        std::stringstream ss;
        ss << msg_front << count;
        msg.data = ss.str();
        //发布消息
        pub.publish(msg);
        //加入调试,打印发送的消息
        ROS_INFO("发送的消息:%s",msg.data.c_str());

        //根据前面制定的发送贫频率自动休眠 休眠时间 = 1/频率;
        r.sleep();
        count++;//循环结束前,让 count 自增
        //暂无应用
        ros::spinOnce();
    }


    return 0;
}

2.编写订阅方subscriber,类似于发布方,大体流程一样,最后实现不一样。

  • _ws/src/包名/src/下新建cpp文件。
  • 包含头文件。

  • 初始化ROS节点:要命名唯一。

  • 实例化ROS句柄。

  • 实例化订阅者对象。

  • 处理订阅的消息(编写回调函数)。

  • 设置循环调用函数。

  完整代码如下:

// 1.包含头文件 
#include "ros/ros.h"
#include "std_msgs/String.h"

void doMsg(const std_msgs::String::ConstPtr& msg_p){
    ROS_INFO("我听见:%s",msg_p->data.c_str());
    // ROS_INFO("我听见:%s",(*msg_p).data.c_str());
}
int main(int argc, char  *argv[])
{
    setlocale(LC_ALL,"");
    //2.初始化 ROS 节点:命名(唯一)
    ros::init(argc,argv,"listener");
    //3.实例化 ROS 句柄
    ros::NodeHandle nh;

    //4.实例化 订阅者 对象
    ros::Subscriber sub = nh.subscribe<std_msgs::String>("chatter",10,doMsg);
    //5.处理订阅的消息(回调函数)

    //     6.设置循环调用回调函数
    ros::spin();//循环读取接收的数据,并调用回调函数处理

    return 0;
}

3.配置CMakeLists.txt文件,主要是加入add_executable()和target_link_libraries()。如下

 

add_executable(Hello_pub
  src/Hello_pub.cpp
)
add_executable(Hello_sub
  src/Hello_sub.cpp
)

target_link_libraries(Hello_pub
  ${catkin_LIBRARIES}
)
target_link_libraries(Hello_sub
  ${catkin_LIBRARIES}
)

 

 4.执行,启动roscore,启动发布节点,启动订阅节点。

补充1:vscode 中的 main 函数 声明 int main(int argc, char const *argv[]){},默认生成 argv 被 const 修饰,需要去除该修饰符。

补充2:订阅时,第一条数据丢失。

原因: 发送第一条数据时, publisher 还未在 roscore 注册完毕。

解决: 注册后,加入休眠 ros::Duration(3.0).sleep(); 延迟第一条数据的发送。

补充3:可以使用命令行 rqt_graph 查看节点关系。

 

二、服务通信

所谓服务通信,即以请求响应的方式实现不同节点之间数据交互的通信模式。主要用于偶然的、对实时性有要求、有一定逻辑处理需求的数据传输场景。

服务通信较之于话题通信更简单些,理论模型如下图所示,该模型中涉及到三个角色:

  • ROS master(管理者)
  • Server(服务端)
  • Client(客户端)

ROS Master 负责保管 Server 和 Client 注册的信息,并匹配话题相同的 Server 与 Client ,帮助 Server 与 Client 建立连接,连接建立后,Client 发送请求信息,Server 返回响应信息。

整个流程如下:

  • Server注册

  • Client注册

  • ROS Master实现信息匹配

  • Client发送请求

  • Server发送响应

编写服务通信的自定义msg类型的步骤一般如下:

  • 按固定格式创建srv文件。在包名/下创建srv文件夹,并在srv文件夹下创建.srv文件,如下
    # 客户端请求时发送的两个数字
    int32 num1
    int32 num2
    ---
    # 服务器响应发送的数据
    int32 sum
    
  • 编辑配置文件。package.xml中添加编译依赖与执行依赖,如下
      <build_depend>message_generation</build_depend>
      <exec_depend>message_runtime</exec_depend>
    
  • CMakeLists.txt编辑srv相关配置,如下
    find_package(catkin REQUIRED COMPONENTS
      roscpp
      rospy
      std_msgs
      message_generation
    )
    # 需要加入 message_generation,必须有 std_msgs
    
    add_service_files(
      FILES
      AddInts.srv
    )
    
    generate_messages(
      DEPENDENCIES
      std_msgs
    )
    
  • 编译后发现生成了头文件。工作空间/devel/include/包名/xxx.h。
  • 将上述头文件的所在路径写入c_pp_properties.json中的includePath段中,如下
     "/home/chenjianlin/demo3_ws/devel/include/**"
    

     

 

编写并实现服务通信的步骤如下:

1.编写服务端实现。

  • 工作空间/src/包名/src/下新建cpp文件。

  • 包含头文件。

  • 初始化ROS节点。

  • 创建ROS句柄。
  • 创建服务对象。

  • 回调函数处理请求并产生响应。

  • 由于请求有多个,需要调用ros::spin()

  完整代码如下

#include "ros/ros.h"
#include "demo03_server_client/AddInts.h"

// bool 返回值由于标志是否处理成功
bool doReq(demo03_server_client::AddInts::Request& req,
          demo03_server_client::AddInts::Response& resp){
    int num1 = req.num1;
    int num2 = req.num2;

    ROS_INFO("服务器接收到的请求数据为:num1 = %d, num2 = %d",num1, num2);

    //逻辑处理
    if (num1 < 0 || num2 < 0)
    {
        ROS_ERROR("提交的数据异常:数据不可以为负数");
        return false;
    }

    //如果没有异常,那么相加并将结果赋值给 resp
    resp.sum = num1 + num2;
    return true;


}

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");
    // 2.初始化 ROS 节点
    ros::init(argc,argv,"AddInts_Server");
    // 3.创建 ROS 句柄
    ros::NodeHandle nh;
    // 4.创建 服务 对象
    ros::ServiceServer server = nh.advertiseService("AddInts",doReq);
    ROS_INFO("服务已经启动....");
    //     5.回调函数处理请求并产生响应
    //     6.由于请求有多个,需要调用 ros::spin()
    ros::spin();
    return 0;
}

 2.编写客户端实现。

  • 工作空间/src/包名/src/下新建cpp文件。

  • 包含头文件。

  • 初始化ROS节点。

  • 创建ROS句柄。
  • 创建客户端对象。

  • 请求服务,接收响应。

  完整代码如下

// 1.包含头文件
#include "ros/ros.h"
#include "demo03_server_client/AddInts.h"

int main(int argc, char *argv[])
{
    setlocale(LC_ALL,"");

    // 调用时动态传值,如果通过 launch 的 args 传参,需要传递的参数个数 +3
    if (argc != 3)
    // if (argc != 5)//launch 传参(0-文件路径 1传入的参数 2传入的参数 3节点名称 4日志路径)
    {
        ROS_ERROR("请提交两个整数");
        return 1;
    }


    // 2.初始化 ROS 节点
    ros::init(argc,argv,"AddInts_Client");
    // 3.创建 ROS 句柄
    ros::NodeHandle nh;
    // 4.创建 客户端 对象
    ros::ServiceClient client = nh.serviceClient<demo03_server_client::AddInts>("AddInts");
    //等待服务启动成功
    //方式1
    ros::service::waitForService("AddInts");
    //方式2
    // client.waitForExistence();
    // 5.组织请求数据
    demo03_server_client::AddInts ai;
    ai.request.num1 = atoi(argv[1]);
    ai.request.num2 = atoi(argv[2]);
    // 6.发送请求,返回 bool 值,标记是否成功
    bool flag = client.call(ai);
    // 7.处理响应
    if (flag)
    {
        ROS_INFO("请求正常处理,响应结果:%d",ai.response.sum);
    }
    else
    {
        ROS_ERROR("请求处理失败....");
        return 1;
    }

    return 0;
}

 3.配置CMakeLists.txt。

add_executable(AddInts_Server src/AddInts_Server.cpp)
add_executable(AddInts_Client src/AddInts_Client.cpp)


add_dependencies(AddInts_Server ${PROJECT_NAME}_gencpp)
add_dependencies(AddInts_Client ${PROJECT_NAME}_gencpp)


target_link_libraries(AddInts_Server
  ${catkin_LIBRARIES}
)
target_link_libraries(AddInts_Client
  ${catkin_LIBRARIES}
)

 4.执行。先启动roscore,再启动rosrun 包名 服务端

然后再启动rosrun 包名 客户端 参数1 参数2

 

优化补充:再客户端发送请求前添加client.waitForExistence();可以阻塞等待服务端启动后才继续执行。

忙碌充实的周末,暂时先到这里了:-D。

posted @ 2021-03-14 22:58  长歌弦断有谁听  阅读(592)  评论(0)    收藏  举报