01ROS通信

ROS通信

话题通信

话题通信是ROS中使用频率最高的一种通信模式,话题通信是基于发布订阅模式的,也即:一个节点发布消息,另一个节点订阅该消息。话题通信的应用场景也极其广泛,比如下面一个常见场景:

机器人在执行导航功能,使用的传感器是激光雷达,机器人会采集激光雷达感知到的信息并计算,然后生成运动控制信息驱动机器人底盘运动。

在上述场景中,就不止一次使用到了话题通信。

  • 以激光雷达信息的采集处理为例,在 ROS 中有一个节点需要实时的发布当前雷达采集到的数据,导航模块中也有节点会订阅并解析雷达数据。
  • 再以运动消息的发布为例,导航模块会根据传感器采集的数据时时的计算出运动控制信息并发布给底盘,底盘也可以有一个节点订阅运动信息并最终转换成控制电机的脉冲信号。

以此类推,像雷达、摄像头、GPS… 等等一些传感器数据的采集,也都是使用了话题通信,换言之,话题通信适用于不断更新的数据传输相关的应用场景。


概念

以发布订阅的方式实现不同节点之间数据交互的通信模式。

作用

用于不断更新的、少逻辑处理的数据传输场景。

例子:

Snipaste_2022-03-05_17-01-17

Snipaste_2022-03-05_17-00-16

注意:

  1. 使用的协议有RPC和TCP
  2. 步骤0和步骤1没有顺序关系
  3. talker和listener都可以存在多个
  4. talker和listener建立连接后,master就可以关闭了
  5. 上述实现流程已经分装了,以后可以直接调用

话题通信应用时的关注点:

  1. 大部分实现已经被封装了
  2. 话题设置
  3. 关注发布者实现
  4. 关注订阅者实现
  5. 关注消息载体

话题通信实现(C++)

需求:

编写发布订阅实现,要求发布方以10HZ(每秒10次)的频率发布文本消息,订阅方订阅消息并将消息内容打印输出。

分析:

在模型实现中,ROS master 不需要实现,而连接的建立也已经被封装了,需要关注的关键点有三个:

  1. 发布方
  2. 接收方
  3. 数据(此处为普通文本)

流程:

  1. 编写发布方实现;
  2. 编写订阅方实现;
  3. 编辑配置文件;
  4. 编译并执行。
发布方
  1. 创建一个空文件夹

Snipaste_2022-03-05_17-12-54

  1. 创建src文件夹

  2. catkin_make

  3. 打开vscode

Snipaste_2022-03-05_17-16-26

  1. 配置编译命令

快捷键 ctrl + shift + B 调用编译,选择:catkin_make:build

Snipaste_2022-03-05_17-28-13

可以点击配置设置为默认,修改.vscode/tasks.json 文件

{
// 有关 tasks.json 格式的文档,请参见
    // https://go.microsoft.com/fwlink/?LinkId=733558
    "version": "2.0.0",
    "tasks": [
        {
            "label": "catkin_make:debug", //代表提示的描述性信息
            "type": "shell",  //可以选择shell或者process,如果是shell代码是在shell里面运行一个命令,如果是process代表作为一个进程来运行
            "command": "catkin_make",//这个是我们需要运行的命令
            "args": [],//如果需要在命令后面加一些后缀,可以写在这里,比如-DCATKIN_WHITELIST_PACKAGES=“pac1;pac2”
            "group": {"kind":"build","isDefault":true},
            "presentation": {
                "reveal": "always"//可选always或者silence,代表是否输出信息
            },
            "problemMatcher": "$msCompile"
        }
    ]
}

改完之后编译一下ctrl + shift + B,之后在编译代码直接用ctrl + shift + B就行

  1. 选定 src 右击 —> create catkin package
  2. 创建功能包名话题通信: plump_pub_sub

Snipaste_2022-03-05_17-19-45

  1. 导入需要的依赖: roscpp rospy std_msgs

Snipaste_2022-03-05_17-21-38

如果没有代码提示

修改 .vscode/c_cpp_properties.json

设置 “cppStandard”: “c++17”

PS2: main 函数的参数不可以被 const 修饰

PS3: 当ROS__INFO 终端输出有中文时,会出现乱码

INFO: ???

解决办法:在函数开头加入下面代码的任意一句

//解决中文乱码
setlocale(LC_ALL,"");
  1. 编写发布代码
# include "ros/ros.h"
# include "std_msgs/String.h"

/*

    实现流程:
        1.包含头文件 :
            ROS中文类型  ---》std_msgs/String.h
        2.初始化 ROS 节点:命名(唯一)
        3.实例化 ROS 句柄
        4.实例化 发布者 对象
        5.组织被发布的数据,并编写逻辑发布数据

*/

int main(int argc, char  *argv[])
{
    //2.初始化 ROS 节点:命名(唯一)
     // 参数1和参数2 后期为节点传值会使用
    // 参数3 是节点名称,是一个标识符,需要保证运行后,在 ROS 网络拓扑中唯一
        ros::init(argc,argv,"nan");

    //3.实例化 ROS 句柄
        ros::NodeHandle nan;//该类封装了 ROS 中的一些常用功能

    //4.实例化 发布者 对象
    //泛型: 发布的消息类型
    //参数1: 要发布到的话题
    //参数2: 队列中最大保存的消息数,超出此阀值时,先进的先销毁(时间早的先销毁)
        ros::Publisher pub = nan.advertise<std_msgs::String>("fang",10);
    
    //5.组织被发布的数据,并编写逻辑发布数据
    //数据(动态组织)
    //先创建被发布的消息
        std_msgs::String msg;
    //编写循环 循环中发布数据
        while (ros::ok)//节点不死
        {
            msg.data = "hello";
            pub.publish(msg);
        }
        

    return 0;
}

  1. 配置CMakeLists.txt

C++ 配置:

add_executable(节点名称
  src/C++源文件名.cpp
)
target_link_libraries(节点名称
  ${catkin_LIBRARIES}
)

Python 配置:

catkin_install_python(PROGRAMS scripts/自定义文件名.py
  DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)

Snipaste_2022-03-05_18-08-16

改好后编译运行一下。

  1. 打开终端 调用roscore
source ./devel/setup.bash
rosrun 包名 C++节点

Snipaste_2022-03-05_18-16-15

  1. 可以使用命令验证:

rostopic echo 话题名称

Snipaste_2022-03-05_18-21-34

订阅方
/*
    需求: 实现基本的话题通信,一方发布数据,一方接收数据,
         实现的关键点:
         1.发送方
         2.接收方
         3.数据(此处为普通文本)


    消息订阅方:
        订阅话题并打印接收到的消息

    实现流程:
        1.包含头文件 
        2.初始化 ROS 节点:命名(唯一)
        3.实例化 ROS 句柄
        4.实例化 订阅者 对象
        5.处理订阅的消息(回调函数)
        6.设置循环调用回调函数

*/
// 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;
}

注意

补充0:

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

补充1:

ros/ros.h No such file or directory …

检查 CMakeList.txt find_package 出现重复,删除内容少的即可

参考资料:https://answers.ros.org/question/237494/fatal-error-rosrosh-no-such-file-or-directory/

补充2:

find_package 不添加一些包,也可以运行啊, ros.wiki 答案如下

You may notice that sometimes your project builds fine even if you did not call find_package with all dependencies. This is because catkin combines all your projects into one, so if an earlier project calls find_package, yours is configured with the same values. But forgetting the call means your project can easily break when built in isolation.

补充3:

订阅时,第一条数据丢失

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

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

话题通信实现(python)

发布方
"""
实现流程:
        1.导包 
        2.初始化 ROS 节点:命名(唯一)
        3.实例化 发布者 对象
        4.组织被发布的数据,并编写逻辑发布数据


"""
#1.导包 
import rospy
from std_msgs.msg import String

if __name__ == "__main__":
    #2.初始化 ROS 节点:命名(唯一)
    rospy.init_node("talker_p")
    #3.实例化 发布者 对象
    pub = rospy.Publisher("chatter",String,queue_size=10)
    #4.组织被发布的数据,并编写逻辑发布数据
    msg = String()  #创建 msg 对象
    msg_front = "hello 你好"
    count = 0  #计数器 
    # 设置循环频率
    rate = rospy.Rate(1)
    while not rospy.is_shutdown():

        #拼接字符串
        msg.data = msg_front + str(count)

        pub.publish(msg)
        rate.sleep()
        rospy.loginfo("写出的数据:%s",msg.data)
        count += 1
订阅方
"""
实现流程:
        1.导包 
        2.初始化 ROS 节点:命名(唯一)
        3.实例化 订阅者 对象
        4.处理订阅的消息(回调函数)
        5.设置循环调用回调函数



"""
#1.导包 
import rospy
from std_msgs.msg import String

def doMsg(msg):
    rospy.loginfo("I heard:%s",msg.data)

if __name__ == "__main__":
    #2.初始化 ROS 节点:命名(唯一)
    rospy.init_node("listener_p")
    #3.实例化 订阅者 对象
    sub = rospy.Subscriber("chatter",String,doMsg,queue_size=10)
    #4.处理订阅的消息(回调函数)
    #5.设置循环调用回调函数
    rospy.spin()
3.添加可执行权限

终端下进入 scripts 执行:chmod +x *.py

4.配置 CMakeLists.txt
catkin_install_python(PROGRAMS
  scripts/talker_p.py
  scripts/listener_p.py
  DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)

PS:可以使用 rqt_graph 查看节点关系。

rosrun rqt_graph rqt_graph

话题通信自定义msg

在 ROS 通信协议中,数据载体是一个较为重要组成部分,ROS 中通过 std_msgs 封装了一些原生的数据类型,比如:String、Int32、Int64、Char、Bool、Empty… 但是,这些数据一般只包含一个 data 字段,结构的单一意味着功能上的局限性,当传输一些复杂的数据,比如: 激光雷达的信息… std_msgs 由于描述性较差而显得力不从心,这种场景下可以使用自定义的消息类型

msgs只是简单的文本文件,每行具有字段类型和字段名称,可以使用的字段类型有:

  • int8, int16, int32, int64 (或者无符号类型: uint*)
  • float32, float64
  • string
  • time, duration
  • other msg files
  • variable-length array[] and fixed-length array[C]

ROS中还有一种特殊类型:Header,标头包含时间戳和ROS中常用的坐标帧信息。会经常看到msg文件的第一行具有Header标头

**需求:**创建自定义消息,该消息包含人的信息:姓名、身高、年龄等。

流程:

  1. 按照固定格式创建 msg 文件
  2. 编辑配置文件
  3. 编译生成可以被 Python 或 C++ 调用的中间文件
定义msg文件

功能包下新建 msg 目录,添加文件 Person.msg

msg文件

编辑配置文件

package.xml中添加编译依赖与执行依赖

  <build_depend>message_generation</build_depend>
  <exec_depend>message_runtime</exec_depend>
  <!-- 
  exce_depend 以前对应的是 run_depend 现在非法
  -->

例子:

  <buildtool_depend>catkin</buildtool_depend>
  <build_depend>roscpp</build_depend>
  <build_depend>rospy</build_depend>
  <build_depend>std_msgs</build_depend>
  <build_depend>message_generation</build_depend>

  <build_export_depend>roscpp</build_export_depend>
  <build_export_depend>rospy</build_export_depend>
  <build_export_depend>std_msgs</build_export_depend>


  <exec_depend>roscpp</exec_depend>
  <exec_depend>rospy</exec_depend>
  <exec_depend>std_msgs</exec_depend>
  <exec_depend>message_runtime</exec_depend>

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
)
编译

编译后的中间文件查看:

C++ 需要调用的中间文件(…/工作空间/devel/include/包名/xxx.h)

Person

Python 需要调用的中间文件(…/工作空间/devel/lib/python3/dist-packages/包名/msg)

06vscode_自定义消息的中间文件(Python)

后续调用相关 msg 时,是从这些中间文件调用的

话题通信自定义msg调用A(C++)

需求:

编写发布订阅实现,要求发布方以10HZ(每秒10次)的频率发布自定义消息,订阅方订阅自定义消息并将消息内容打印输出。

分析:

在模型实现中,ROS master 不需要实现,而连接的建立也已经被封装了,需要关注的关键点有三个:

  1. 发布方
  2. 接收方
  3. 数据(此处为自定义消息)

流程:

  1. 编写发布方实现;
  2. 编写订阅方实现;
  3. 编辑配置文件;
  4. 编译并执行。
vscode 配置

为了方便代码提示以及避免误抛异常,需要先配置 vscode,将前面生成的 head 文件路径配置进 c_cpp_properties.json 的 includepath属性:

{
    "configurations": [
        {
            "browse": {
                "databaseFilename": "",
                "limitSymbolsToIncludedHeaders": true
            },
            "includePath": [
                "/opt/ros/noetic/include/**",
                "/usr/include/**",
                "/xxx/yyy工作空间/devel/include/**" ,//配置 head 文件的路径
                "/home/zjh/ros_demo/demo02_topic_cn/src/**"//例子
            ],
            "name": "ROS",
            "intelliSenseMode": "gcc-x64",
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "c11",
            "cppStandard": "c++17"
        }
    ],
    "version": 4
}
发布方
#include "ros/ros.h"
#include "plumbing_pub_sub/Person.h"

/*
    发布方:发布人的消息
    1.包含文件
    2.初始化ROS节点
    3.创建节点句柄
    4.创建发布者对象
    5.编写发布逻辑,发布数据

*/

int main(int argc, char  *argv[])
{
    
     //中文乱码
    setlocale(LC_ALL,"");
    ROS_INFO("这是消息发布方:");

    // 2.初始化ROS节点
    ros::init(argc,argv,"wo");

    //3.创建节点句柄
    ros::NodeHandle nh;

    //4.创建发布者对象
    ros::Publisher pub = nh.advertise<plumbing_pub_sub::Person>("nihao",10);

    // 5.编写发布逻辑,发布数据
    //5.1创建被发布的数据
        

    plumbing_pub_sub::Person p;
    p.name="小子";
    p.age=11;
    p.height=170.28;

    //编写发布速度
    ros::Rate rate(2);

    while(ros::ok()){

        p.age +=1;
        pub.publish(p);
        ROS_INFO("name: %s, age: %d, height: %.2f m",p.name.c_str(),p.age,p.height);

        rate.sleep();
        ros::spinOnce();

    }

    

    return 0;
}

测试:roscore

​ rosrun 报名 文件cpp名

msg_person_cpp

订阅方
#include "ros/ros.h"
#include "plumbing_pub_sub/Person.h"

/*
    订阅方:订阅人的消息
    1.包含头文件
    2.初始化ROS节点
    3.创建节点句柄
    4.回调函数处理数据
    5.spin()

*/


void doPerson(const plumbing_pub_sub::Person::ConstPtr&  per)
{
    ROS_INFO("订阅人的信息:%s,%d,%.2f",per->name.c_str(),per->age,per->height);

}


int main(int argc, char  *argv[])
{

        //中文乱码
    setlocale(LC_ALL,"");
    //初始化ROS节点
    ros::init(argc,argv,"ni");

    // 3.创建节点句柄
    ros::NodeHandle nh;
    //实例化订阅者对象
    ros::Subscriber sub = nh.subscribe<plumbing_pub_sub::Person>("nihao",10,doPerson);

    // 4.回调函数处理数据

    //spin()
    ros::spin();


    return 0;
}

配置 CMakeLists.txt

需要添加 add_dependencies 用以设置所依赖的消息相关的中间文件。

add_executable(person_talker src/person_talker.cpp)
add_executable(person_listener src/person_listener.cpp)



add_dependencies(person_talker ${PROJECT_NAME}_generate_messages_cpp)
add_dependencies(person_listener ${PROJECT_NAME}_generate_messages_cpp)


target_link_libraries(person_talker
  ${catkin_LIBRARIES}
)
target_link_libraries(person_listener
  ${catkin_LIBRARIES}
)
执行

1.启动 roscore;

2.启动发布节点;

3.启动订阅节点。

运行结果与引言部分的演示案例2类似。


PS:可以使用 rqt_graph 查看节点关系。

rqt_msg_person

话题通信自定义msg调用B(Python)

需求:

编写发布订阅实现,要求发布方以1HZ(每秒1次)的频率发布自定义消息,订阅方订阅自定义消息并将消息内容打印输出。

分析:

在模型实现中,ROS master 不需要实现,而连接的建立也已经被封装了,需要关注的关键点有三个:

  1. 发布方
  2. 接收方
  3. 数据(此处为自定义消息)

流程:

  1. 编写发布方实现;
  2. 编写订阅方实现;
  3. 为python文件添加可执行权限;
  4. 编辑配置文件;
  5. 编译并执行。
vscode配置

为了方便代码提示以及误抛异常,需要先配置 vscode,将前面生成的 python 文件路径配置进 settings.json

{
    "python.autoComplete.extraPaths": [
        "/opt/ros/noetic/lib/python3/dist-packages",
        "/xxx/yyy工作空间/devel/lib/python3/dist-packages"
    ]
}
发布方
#! /usr/bin/env python
# encoding=utf-8
import rospy
# from std_msgs.msg import String #发布消息类型
from plumbing_pub_sub.msg import Person


if __name__ =="__main__":
    #1.初始化ros节点
    rospy.init_node("wowo")

    #2.创建发布者对象
    pub = rospy.Publisher("zaima",Person,queue_size=10)

    #3.组织消息
    # 3.1c创建Person对象
    psn = Person()
    psn.name="我看行"
    psn.age=18
    psn,height=178.87

    # 3.2创建rate对象
    rate = rospy.Rate(1)

    #3.3循环发布数据
    while not rospy.is_shutdown():
        pub.publish(psn)
        rate.sleep()
        rospy.loginfo("姓名:%s,年龄:%d,身高:%.2f",psn.name,psn.age,psn.height)



订阅方
#! /usr/bin/env python
# encoding=utf-8
"""
        1.导包 
        2.初始化 ROS 节点:命名(唯一)
        3.实例化 订阅者 对象
        4.处理订阅的消息(回调函数)
        5.设置循环调用回调函数
"""

import rospy
from plumbing_pub_sub import Person

def doPerson(p):
    rospy.loginfo("发布者的信息:%s,%d,%.2f",p.name,p.age,p.height)

if __name__ == "__main__":
    # 2.初始化 ROS 节点:命名(唯一)
    rospy.init_node("nini")

    # 3.实例化 订阅者 对象
    sub = rospy.Subscriber("zaima",Person , doPerson,queue_size=10)

        # 4.处理订阅的消息(回调函数)
        # 5.设置循环调用回调函数
    rospy.spin()
权限设置

终端下进入 scripts 执行:chmod +x *.py

4.配置 CMakeLists.txt
catkin_install_python(PROGRAMS
  scripts/talker_p.py
  scripts/listener_p.py
  scripts/person_talker.py
  scripts/person_listener.py
  DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
5.执行

1.启动 roscore;

2.启动发布节点;

3.启动订阅节点。

运行结果与引言部分的演示案例2类似。


PS:可以使用 rqt_graph 查看节点关系。

服务通信

服务通信理论模型

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

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

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

img

Snipaste_2022-03-09_17-40-14

整个流程由以下步骤实现:

Server注册

Server 启动后,会通过RPC在 ROS Master 中注册自身信息,其中包含提供的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。

Client注册

Client 启动后,也会通过RPC在 ROS Master 中注册自身信息,包含需要请求的服务的名称。ROS Master 会将节点的注册信息加入到注册表中。

ROS Master实现信息匹配

ROS Master 会根据注册表中的信息匹配Server和 Client,并通过 RPC 向 Client 发送 Server 的 TCP 地址信息。

Client发送请求

Client 根据步骤2 响应的信息,使用 TCP 与 Server 建立网络连接,并发送请求数据。

Server发送响应

Server 接收、解析请求的数据,并产生响应结果返回给 Client。

注意:

1.客户端请求被处理时,需要保证服务器已经启动;

2.服务端和客户端都可以存在多个。

服务通信自定义srv

需求:

服务通信中,客户端提交两个整数至服务端,服务端求和并响应结果到客户端,请创建服务器与客户端通信的数据载体。

流程:

srv 文件内的可用数据类型与 msg 文件一致,且定义 srv 实现流程与自定义 msg 实现流程类似:

  1. 按照固定格式创建srv文件
  2. 编辑配置文件
  3. 编译生成中间文件

.定义srv文件

服务通信中,数据分成两部分,请求与响应,在 srv 文件中请求和响应使用---分割,具体实现如下:

功能包下新建 srv 目录,添加 xxx.srv 文件,内容:

# 客户端请求时发送的两个数字
int32 num1
int32 num2
---
# 服务器响应发送的数据
int32 sum

编辑配置文件

package.xml中添加编译依赖与执行依赖

  <build_depend>message_generation</build_depend>
  <exec_depend>message_runtime</exec_depend>
  <!-- 
  exce_depend 以前对应的是 run_depend 现在非法
  -->

如下:

  <buildtool_depend>catkin</buildtool_depend>
  <build_depend>roscpp</build_depend>
  <build_depend>rospy</build_depend>
  <build_depend>std_msgs</build_depend>
  <build_depend>message_generation</build_depend>


  <build_export_depend>roscpp</build_export_depend>
  <build_export_depend>rospy</build_export_depend>
  <build_export_depend>std_msgs</build_export_depend>


  <exec_depend>roscpp</exec_depend>
  <exec_depend>rospy</exec_depend>
  <exec_depend>std_msgs</exec_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
)
catkin_package(
#  INCLUDE_DIRS include
#  LIBRARIES plumbing_server_client
 CATKIN_DEPENDS roscpp rospy std_msgs message_runtime
#  DEPENDS system_lib
)

注意: 官网没有在 catkin_package 中配置 message_runtime,经测试配置也可以

编译

编译后的中间文件查看:

C++ 需要调用的中间文件(…/工作空间/devel/include/包名/xxx.h)

img

Python 需要调用的中间文件(…/工作空间/devel/lib/python3/dist-packages/包名/srv)

img

后续调用相关 srv 时,是从这些中间文件调用的

服务通信自定义srv调用A(C++)

需求:

编写服务通信,客户端提交两个整数至服务端,服务端求和并响应结果到客户端。

分析:

在模型实现中,ROS master 不需要实现,而连接的建立也已经被封装了,需要关注的关键点有三个:

  1. 服务端
  2. 客户端
  3. 数据

流程:

  1. 编写服务端实现;
  2. 编写客户端实现;
  3. 编辑配置文件;
  4. 编译并执行。
vscode配置

需要像之前自定义 msg 实现一样配置c_cpp_properies.json 文件,如果以前已经配置且没有变更工作空间,可以忽略,如果需要配置,配置方式与之前相同:

{
    "configurations": [
        {
            "browse": {
                "databaseFilename": "",
                "limitSymbolsToIncludedHeaders": true
            },
            "includePath": [
                "/opt/ros/noetic/include/**",
                "/usr/include/**",
                "/xxx/yyy工作空间/devel/include/**" ,//配置 head 文件的路径 
                "/home/zjh/ros_demo/demo02_topic_cn/src/**"//例子
            ],
            "name": "ROS",
            "intelliSenseMode": "gcc-x64",
            "compilerPath": "/usr/bin/gcc",
            "cStandard": "c11",
            "cppStandard": "c++17"
        }
    ],
    "version": 4
}
服务端
#include "ros/ros.h"
#include "plumbing_server_client/AddInts.h"

/*
    服务端实现:解析客户端提交的数据,并运算产生响应
    1.包含头文件
    2.初始化ros节点
    3.创建节点句柄
    4.创建一个服务对象
    5.处理请求并产生响应
    6.spin()
*/
bool doNum(plumbing_server_client::AddInts::Request &req,
                plumbing_server_client::AddInts::Response &resp){

                    int num1 = req.num1;
                    int num2 = req.num2;

                    resp.sum = num1+num2;
                    ROS_INFO("服务器接受到的数据为:num1=%d,num2=%d",num1,num2);
                    ROS_INFO("求和为:sum=%d",resp.sum);

                    return true;


                }
int main(int argc, char *argv[])
{
    
    setlocale(LC_ALL,"");
    //     1.包含头文件
    // 2.初始化ros节点
    ros::init(argc,argv,"server");
    // 3.创建节点句柄
    ros::NodeHandle nh;
    // 4.创建一个服务对象
    ros::ServiceServer ser = nh.advertiseService("xi",doNum);
    ROS_INFO("服务端已经启动!");
    // 5.处理请求并产生响应
    // 6.spin()
   ros:: spin();

    return 0;
}

测试:
1. rocore
2. source ./devel/setup.bash
3. rosrun 包名 项目名
4. rosservice call 话题名称 (Tab补齐两个参数)num1 num2

屏幕截图_4

客户端
#include "ros/ros.h"
#include "plumbing_server_client/AddInts.h"

/*
    客户端实现:解析客户端提交的数据,并运算产生响应
    1.包含头文件
    2.初始化ros节点
    3.创建节点句柄
    4.创建一个客户端对象
    5.提交请求并产生响应
    6.spin()

    实现参数的动态提交:
        1.格式:rosrun xxxx xxxx 12 31
        2.节点执行时,需要获取命令中的参数,并组织进request

    问题:
        如果先启动客户端,那么会请求异常。
    需求:
        如果先启动客户端,不要抛出异常,而是挂起,等服务器启动后,再正常请求。
    解决:
        在ROS中内置了相关函数,这些函数可以让客户端启动后挂起等待服务端启动。
            //方式1
             // ros::service::waitForService("xi");

             //方式2
             client.waitForExistence();
*/

int main(int argc, char  *argv[])
{
     setlocale(LC_ALL,"");
    //优化实现,获取命令中的参数
    // if (argc != 5)//launch 传参(0-文件路径 1传入的参数 2传入的参数 3节点名称 4日志路径)
    if( argc != 3)
    {
        ROS_INFO("请提交两个数");
        return 1;
    }

    //     2.初始化ros节点
    ros::init(argc,argv,"kehu");
    // 3.创建节点句柄
    ros::NodeHandle nh;
    // 4.创建一个客户端对象
    ros::ServiceClient client = nh.serviceClient<plumbing_server_client::AddInts>("xi");
    // 5.提交请求并产生响应
    plumbing_server_client::AddInts ai;
    //5.1组织请求
    ai.request.num1 =atoi(argv[1]);//atoi() 字符串 >>> 整型
    ai.request.num2 =atoi(argv[2]);
    //5.2处理响应
    //等待服务启动成功
    //方式1
    // ros::service::waitForService("xi");

    //方式2
    client.waitForExistence();
    bool flag = client.call(ai);
    if(flag){

        ROS_INFO("响应成功!");
        ROS_INFO("响应结果:sum=%d",ai.response.sum);

    }
    else{
        ROS_INFO("处理失败!!!");
    }
    // 6.spin()
    return 0;
}

配置 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}
)
执行

流程:

  • 需要先启动服务:rosrun 包名 服务
  • 然后再调用客户端 :rosrun 包名 客户端 参数1 参数2

结果:

服务响应

会根据提交的数据响应相加后的结果。

注意:

如果先启动客户端,那么会导致运行失败

优化:

在客户端发送请求前添加:client.waitForExistence();

或:ros::service::waitForService("AddInts");

这是一个阻塞式函数,只有服务启动成功后才会继续执行

此处可以使用 launch 文件优化,但是需要注意 args 传参特点

​ //优化实现,获取命令中的参数

​ // if (argc != 5)//launch 传参(0-文件路径 1传入的参数 2传入的参数 3节点名称 4日志路径)

实现参数的动态提交:

​ 1.格式:rosrun xxxx xxxx 12 31

​ 2.节点执行时,需要获取命令中的参数,并组织进request

问题:

​ 如果先启动客户端,那么会请求异常。

需求:

​ 如果先启动客户端,不要抛出异常,而是挂起,等服务器启动后,再正常请求。

解决:

​ 在ROS中内置了相关函数,这些函数可以让客户端启动后挂起等待服务端启动。

​ //方式1

​ // ros::service::waitForService(“xi”);

​ //方式2

​ client.waitForExistence();

服务通信自定义srv调用B(Python)

需求:

编写服务通信,客户端提交两个整数至服务端,服务端求和并响应结果到客户端。

分析:

在模型实现中,ROS master 不需要实现,而连接的建立也已经被封装了,需要关注的关键点有三个:

  1. 服务端
  2. 客户端
  3. 数据

流程:

  1. 编写服务端实现;
  2. 编写客户端实现;
  3. 为python文件添加可执行权限;
  4. 编辑配置文件;
  5. 编译并执行。
vscode配置

需要像之前自定义 msg 实现一样配置settings.json 文件,如果以前已经配置且没有变更工作空间,可以忽略,如果需要配置,配置方式与之前相同:

{
    "python.autoComplete.extraPaths": [
        "/opt/ros/noetic/lib/python3/dist-packages",
    ]
}
服务端
#! /usr/bin/env python
# encoding=utf-8
"""
    服务器端实现:
        1.导包
        2.初始化 ROS 节点
        3.创建服务对象
        4.回调函数处理请求并产生响应
        5.spin 函数

""" 
import rospy
# from plumbing_server_client.srv import AddInts,AddIntsResquest,AddIntsResponse
from plumbing_server_client.srv import AddInts,AddIntsResponse,AddIntsRequest
# 参数:封装了请求数据
# 返回:响应数据
def donum(req):
    sum = req.num1 + req.num2
    rospy.loginfo("提交的数据为:num1=%d,num2=%d",req.num1,req.num2)
    response = AddIntsResponse()
    response.sum = sum
    return response 

if __name__ == "__main__":
        # 2.初始化 ROS 节点
        rospy.init_node("server_p")
        # 3.创建服务对象
        server = rospy.Service("dasao",AddInts,donum)
        rospy.loginfo("服务器已启动!")
        # 4.回调函数处理请求并产生响应
        # 5.spin 函数
        rospy.spin()

测试流程与C++相同

客户端
#! /usr/bin/env python
# encoding=utf-8
"""
    需求: 
        编写两个节点实现服务通信,客户端节点需要提交两个整数到服务器
        服务器需要解析客户端提交的数据,相加后,将结果响应回客户端,
        客户端再解析

    客户端实现:
        1.导包
        2.初始化 ROS 节点
        3.创建请求对象
        4.发送请求
        5.接收并处理响应

    优化:
        加入数据的动态获取


"""
#1.导包
import rospy
from plumbing_server_client.srv  import *
import sys

if __name__ == "__main__":

    #优化实现
    if len(sys.argv) != 3:
        rospy.logerr("请正确提交参数")
        sys.exit(1)


    # 2.初始化 ROS 节点
    rospy.init_node("AddInts_Client_p")
    # 3.创建请求对象
    client = rospy.ServiceProxy("dasao",AddInts)
    # 请求前,等待服务已经就绪
    # 方式1:
    # rospy.wait_for_service("AddInts")
    # 方式2
    client.wait_for_service()
    # 4.发送请求,接收并处理响应
    # 方式1
    # resp = client(3,4)
    # 方式2
    # resp = client(AddIntsRequest(1,5))
    # 方式3
    req = AddIntsRequest()
    # req.num1 = 100
    # req.num2 = 200 

    #优化
    req.num1 = int(sys.argv[1])
    req.num2 = int(sys.argv[2]) 

    resp = client.call(req)
    rospy.loginfo("响应结果:%d",resp.sum)
设置权限

终端下进入 scripts 执行:chmod +x *.py

配置 CMakeLists.txt

CMakeLists.txt

catkin_install_python(PROGRAMS
  scripts/AddInts_Server_p.py 
  scripts/AddInts_Client_p.py
  DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION}
)
执行

流程:

  • 需要先启动服务:rosrun 包名 服务
  • 然后再调用客户端 :rosrun 包名 客户端 参数1 参数2

结果:

会根据提交的数据响应相加后的结果。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值