ROS:服务通信

话题通信的数据传输是单向的,订阅端被动接收发布端的数据。这时候有人就问了,如果发布端想主动接收数据怎么办。ROS中提供了另一种通信方式:服务通信。

ROS:通信模型

Service通信是双向的,它不仅可以发送消息,同时还会有反馈。所以service包括两部分,一部分是请求方(Clinet),另一部分是应答方/服务提供方(Server)。这时请求方(Client)就会发送一个request,要等待server处理,反馈回一个reply,这样通过类似“请求-应答”的机制完成整个服务通信。

这种通信方式的示意图如下:

  • Node B是server(应答方),提供了一个服务的接口,叫做/Service,我们一般都会用string类型来指定service的名称,类似于topic。
  • Node A向Node B发起了请求,经过处理后得到了反馈。
    在这里插入图片描述

Service是同步通信方式,所谓同步就是说,此时Node A发布请求后会在原地等待reply,直到Node B处理完了请求并且完成了reply,Node A才会继续执行。Node A等待过程中,是处于阻塞状态的成通信。这样的通信模型没有频繁的消息传递,没有冲突与高系统资源的占用,只有接受请求才执行服务,简单而且高效。

一般用于偶然的、临时的、对实时性有要求、有一定逻辑处理需求的数据传输场景。

ROS:topic VS service

名称Topicservice
通信方式异步通信同步通信
实现原理TCP/IPTCP/IP
通信模型Publish-SubscribeRequest-Reply
映射关系Publish-Subscribe(多对多)Request-Reply(多对一)
特点接受者收到数据会回调(Callback)远程过程调用(RPC)服务器端的服务
应用场景连续、高频的数据发布偶尔使用的功能/具体的任务
举例激光雷达、里程计发布数据开关传感器、拍照、逆解计算

注意:远程过程调用(Remote Procedure Call,RPC),可以简单通俗的理解为在一个进程里调用另一个进程的函数。

ROS:Srv是个什么鬼

类似msg文件,srv文件是用来描述服务(service数据类型的,service通信的数据格式定义在*.srv中。它声明了一个服务,包括请求(request)和响应(reply)两部分。

srv文件格式很固定,第一行是请求的格式,中间用—隔开,第三行是应答的格式。

举个例子:

  • msgs_demo/srv/DetectHuman.srv
    • 作用:用来查询当前深度摄像头中的人体姿态和关节数的
    • 请求为是否开始检测
    • 应答为一个数组:
      • 数组的每个元素为某个人的姿态(HumanPose)。
      • 而对于人的姿态,其实是一个msg,所以srv可以嵌套msg在其中,但它不能嵌套srv。
bool start_detect
---
my_pkg/HumanPose[] pose_data
  • msgs_demo/msg/HumanPose.msg
std_msgs/Header header
string uuid
int32 number_of_joints
my_pkg/JointPose[]joint_data
  • msgs_demo/msg/JointPose.msg
string joint_name
geometry_msgs/Pose pose
floar32 confidence

定义完了msg、srv文件,还有重要的一个步骤就是修改package.xml和修改CMakeList.txt。这些文件需要添加一些必要的依赖等,例如:

<build_depend>** message_generation **</build_depend>
<run_depend>** message_runtime **</run_depend>

上述文本中“**”所引就是新添加的依赖。又例如:

find_package(...roscpp rospy std_msgs ** message_generation **)
catkin_package(
...
CATJIN_DEPENDS ** message_runtime ** ...
...)

add_message_file(
FILES
** DetectHuman.srv **
** HumanPose.msg **
** JointPos.msg **)

** generate_messages(DEPENDENCIES std_msgs) **

ROS:常用命令

ROS1:service命令作用
rosservice list查看服务列表
rosservice info打印服务信息
rosservice type打印服务类型
rosservice uri打印服务ROSRPC uri
rosservice find按服务类型查找服务
rosservice call使用所提供的args调用服务
rosservice args打印服务参数
ROS1:srv命令作用
rossrv show显示服务描述
rossrv list列出所有服务
rossrv md5显示服务md5sum
rossrv package列出包中的服务
rossrv packages列出包含服务的包
ROS2:service命令作用
ros2 service list查看服务列表
ros2 service find example_interfaces/srv/AddTwoInts查找使用某一接口的服务
ros2 service type /add_two_ints查看服务接口类型:
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts “{a: 5,b: 10}”手动调用服务(有参)
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts手动调用服务(无参)

ROS2:体验一下

(1)启动服务端
打开终端,运行下面的命令,这个命令用于运行一个服务节点,这个服务的功能是将两个数字相加,给定a,b两个数,返回sum也就是ab之和。

ros2 run examples_rclpy_minimal_service service

(2)使用命令查看服务列表

ros2 service list

(3)手动调用服务

ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 5,b: 10}"

ROS: 建立流程

三个角色:

  • client(客户端):负责向服务端发送请求,并接收服务端发送的数据。
  • server(服务端):负责接收处理请求,并对客户端做出响应。
  • ROS Master(管理者):负责保管客户端和服务端注册的信息,并匹配话题相同的客户端与服务端,帮助客户端与服务端建立连接。

流程如下:
在这里插入图片描述

  • step0:服务端启动,在管理者处注册,其中包含提供的服务的名称。管理者会将节点的注册信息加入到注册表中。
  • step1:客户端启动,在管理者处注册,其中包含请求的服务的名称。管理者会将节点的注册信息加入到注册表中。
  • step2:管理者根据服务端和客户端的服务名称进行匹配,并将服务端的地址信息告知客户端。
  • step3:客户端与服务端进行网络连接,并发送有关请求。
  • step4:服务端处理请求数据,产生响应并发送给客户端。

注意:

  • 客户端请求被处理时,要保证服务端已经启动
  • 服务端和客户端都可以存在多个

ROS2:代码实现

RCLCPP实现

创建功能包和节点

cd chapt3/chapt3_ws/src
ros2 pkg create example_service_rclcpp --build-type ament_cmake --dependencies rclcpp
touch example_service_rclcpp/src/service_server_01.cpp
touch example_service_rclcpp/src/service_client_01.cpp
  • service_server_01.cpp
#include "rclcpp/rclcpp.hpp"

class ServiceServer01 : public rclcpp::Node {
public:
  ServiceServer01(std::string name) : Node(name) {
    RCLCPP_INFO(this->get_logger(), "节点已启动:%s.", name.c_str());
  }

private:
};

int main(int argc, char** argv) {
  rclcpp::init(argc, argv);
  auto node = std::make_shared<ServiceServer01>("service_server_01");
  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}

  • service_client_01.cpp
#include "rclcpp/rclcpp.hpp"

class ServiceClient01 : public rclcpp::Node {
public:
  // 构造函数,有一个参数为节点名称
  ServiceClient01(std::string name) : Node(name) {
    RCLCPP_INFO(this->get_logger(), "节点已启动:%s.", name.c_str());
  }

private:
};

int main(int argc, char** argv) {
  rclcpp::init(argc, argv);
  /*产生一个的节点*/
  auto node = std::make_shared<ServiceClient01>("service_client_01");
  /* 运行节点,并检测退出信号*/
  node->send_request(5, 6);
  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}

  • CMakeLists.txt
add_executable(service_client_01 src/service_client_01.cpp)
ament_target_dependencies(service_client_01 rclcpp)

add_executable(service_server_01 src/service_server_01.cpp)
ament_target_dependencies(service_server_01 rclcpp)

install(TARGETS
  service_server_01
  DESTINATION lib/${PROJECT_NAME}
)

install(TARGETS
  service_client_01
  DESTINATION lib/${PROJECT_NAME}
)

  • 编译测试
cd chapt3/chapt3_ws/
colcon build --packages-select example_service_rclcpp

# 运行 service_server_01
source install/setup.bash
ros2 run example_service_rclcpp service_server_01

# 打开新终端运行  service_client_01
source install/setup.bash
ros2 run example_service_rclcpp service_client_01

服务端实现

导入接口

ament_cmake类型功能包导入消息接口分为三步:

  • 在CMakeLists.txt中导入,具体是先find_packages再ament_target_dependencies
# 这里我们一次性把服务端和客户端对example_interfaces的依赖都加上
find_package(example_interfaces REQUIRED)

add_executable(service_client_01 src/service_client_01.cpp)
ament_target_dependencies(service_client_01 rclcpp example_interfaces)

add_executable(service_server_01 src/service_server_01.cpp)
ament_target_dependencies(service_server_01 rclcpp example_interfaces)

  • 在packages.xml中导入,具体是添加depend标签并将消息接口写入。
<depend>example_interfaces</depend>

  • 在代码中导入,C++中是#include"消息功能包/xxx/xxx.hpp"。
#include "example_interfaces/srv/add_two_ints.hpp"
编写代码
  • create_service,参考rclcpp API文档即可
    • ServiceT,消息接口example_interfaces::srv::AddTwoInts
    • service_name,服务名称
    • callback,回调函数,使用成员函数作为回调函数,std::bind进行转换
    • qos_profile,服务质量配置文件,默认rmw_qos_profile_services_default
    • group,调用服务的回调组,默认nullptr
#include "example_interfaces/srv/add_two_ints.hpp"
#include "rclcpp/rclcpp.hpp"

class ServiceServer01 : public rclcpp::Node {
public:
  ServiceServer01(std::string name) : Node(name) {
    RCLCPP_INFO(this->get_logger(), "节点已启动:%s.", name.c_str());
    // 创建服务
    add_ints_server_ =
      this->create_service<example_interfaces::srv::AddTwoInts>(
        "add_two_ints_srv",
        std::bind(&ServiceServer01::handle_add_two_ints, this,
                  std::placeholders::_1, std::placeholders::_2));
  }

private:
  // 声明一个服务
  rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr
    add_ints_server_;

  // 收到请求的处理函数
  void handle_add_two_ints(
    const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> request,
    std::shared_ptr<example_interfaces::srv::AddTwoInts::Response> response) {
    RCLCPP_INFO(this->get_logger(), "收到a: %ld b: %ld", request->a,
                request->b);
    response->sum = request->a + request->b;
  };
};

测试
cd chapt3_ws/
colcon build --packages-select example_service_rclcpp
source install/setup.bash
ros2 run example_service_rclcpp service_server_01

接着打开一个新的终端

# 你应该可以看到我们声明的服务
ros2 service list
# 使用命令行进行调用
ros2 service call /add_two_ints_srv example_interfaces/srv/AddTwoInts "{a: 5,b: 10}"

在这里插入图片描述

客户端实现

编写代码

写代码之前要先看api文档

  • create_client
  • 发送请求:async_send_request
    • request,请求的消息,这里用于放a,b两个数。
    • CallBack,回调函数,异步接收服务器的返回的函数
  • wait_for_service:这个函数是用于等待服务上线的,这个函数并不在rclcpp::Client中定义,而是在其父类中定义的。
    • 参数就一个,等待的时间
    • 返回值是bool类型的,上线了就是true,不上线就是false。
#include "example_interfaces/srv/add_two_ints.hpp"

class ServiceClient01 : public rclcpp::Node {
public:
  // 构造函数,有一个参数为节点名称
  ServiceClient01(std::string name) : Node(name) {
    RCLCPP_INFO(this->get_logger(), "节点已启动:%s.", name.c_str());
    // 创建客户端
    client_ = this->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints_srv");
  }

  void send_request(int a, int b) {
    RCLCPP_INFO(this->get_logger(), "计算%d+%d", a, b);

    // 1.等待服务端上线
    while (!client_->wait_for_service(std::chrono::seconds(1))) {
      //等待时检测rclcpp的状态
      if (!rclcpp::ok()) {
        RCLCPP_ERROR(this->get_logger(), "等待服务的过程中被打断...");
        return;
      }
      RCLCPP_INFO(this->get_logger(), "等待服务端上线中");
    }

    // 2.构造请求的
    auto request =
      std::make_shared<example_interfaces::srv::AddTwoInts_Request>();
    request->a = a;
    request->b = b;

    // 3.发送异步请求,然后等待返回,返回时调用回调函数
    client_->async_send_request(
      request, std::bind(&ServiceClient01::result_callback_, this,
                         std::placeholders::_1));
  };

private:
  // 声明客户端
  rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr client_;

  void result_callback_(
    rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedFuture
      result_future) {
    auto response = result_future.get();
    RCLCPP_INFO(this->get_logger(), "计算结果:%ld", response->sum);
  }
};

测试

最后还要修改下主函数,用于调用服务端发送请求

int main(int argc, char** argv) {
  rclcpp::init(argc, argv);
  /*产生一个的节点*/
  auto node = std::make_shared<ServiceClient01>("service_client_01");
  /* 运行节点,并检测退出信号*/
  //增加这一行,node->send_request(5, 6);,计算5+6结果
  node->send_request(5, 6);
  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}

  • 编译运行客户端:
cd chapt3_ws/
colcon build --packages-select example_service_rclcpp
source install/setup.bash
ros2 run example_service_rclcpp service_client_01

  • 打开服务端,让服务上线:
source install/setup.bash
ros2 run example_service_rclcpp service_server_01

RCLPY实现

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值