ros2 服务编程实现

ros2 服务编程实现

一、自定义服务接口

服务接口格式及步骤
#客户端发送请求的数据结构定义
int64 a
int64 b
---
#服务端响应结果的数据结构定义
int64 sum
#1.新建`srv`文件夹,并在文件夹下新建`xxx.srv`

#2.在`xxx.srv`下编写服务接口内容并保存

#3.在`CmakeLists.txt`添加依赖和srv文件目录

#4.在`package.xml`中添加`xxx.srv`所需的依赖

#5.编译功能包即可生成`python`与c++头文件

1.创建借钱服务接口

  • 确定数据结构
    string name
    uint32 money
    ---
    bool success
    uint32 money
    

    客户端:

    • 借钱者名字,字符串类型、可以用string表示
    • 金额,整形,可以用uint32表示

    服务端:

    • 是否出借:只有成功和失败两种情况,布尔类型(bool)可表示
    • 出借金额:无符号整形,可以用uint32表示,借钱失败时为0
  • 创建srv文件夹及BorrowMoney.srv消息文件

    srvservice 的缩写

    cd src/village_interfaces
    mkdir srv && cd srv
    touch BorrowMoney.srv
    

    image-20210811162010736

  • 修改CMakeLists.txt

    因为在4.6中我们已经添加过依赖DEPENDENCIESmsg文件了,所以这里我们直接添加一个srv即可

    find_package(rosidl_default_generators REQUIRED)
    rosidl_generate_interfaces(${PROJECT_NAME}
      #---msg---
      "msg/Novel.msg"
      #---srv---
      "srv/BorrowMoney.srv"
      DEPENDENCIES sensor_msgs
     )
    
  • 修改package.xml
      <build_depend>sensor_msgs</build_depend>
      <build_depend>rosidl_default_generators</build_depend>
      <exec_depend>rosidl_default_runtime</exec_depend>
      <member_of_group>rosidl_interface_packages</member_of_group>
    

2.创建买书服务接口

  • 确定数据结构

    用无符号整形表示money大家已经很清楚如何做了,那如何表示小说数组呢?其实只需在类型的后面加上[]中括号。

    uint32 money
    ---
    string[] novels
    
  • 创建srv文件夹及SellNovel.srv消息文件
    cd src/village_interfaces
    cd srv
    touch SellNovel.srv
    
  • 修改CMakeLists.txt

    在上面的基础上,再添加上一行"srv/SellNovel.srv"代码即可:

    rosidl_generate_interfaces(${PROJECT_NAME}
      #---msg---
      "msg/Novel.msg"
      #---srv---
      "srv/BorrowMoney.srv"
      "srv/SellNovel.srv"
       DEPENDENCIES sensor_msgs
     )
    
  • 修改package.xml

    同样的,因为我们已经修改过了,这里就不需要修改了。

二、服务实现(Python)

剧情梗概:
李三(客户端)向李四(服务端)借钱吃麻辣烫。李四外借的规则为:①借钱一定要打欠条,收到欠条才能给钱;②最多借出去100x10%=10块钱

1.编写服务端代码

服务端基本步骤:

#1.导入服务接口
#2.创建服务端回调函数
#3.声明并创建服务端
#4.编写回调函数逻辑处理请求
  • 添加依赖

    导入依赖是为了能够让我们的代码找到对应的接口。

    因为village_li是包类型是ament_python这里只需要在package.xml中加入下面的代码即可:

      <depend>village_interfaces</depend>
    
  • 导入服务接口
    #从村庄接口服务类中导入借钱服务
    from village_interfaces.srv import BorrowMoney
    
  • 创建服务端
    # 新建借钱服务
    self.borrow_server = self.create_service(BorrowMoney, "borrow_money", self.borrow_money_callback)
    
  • 编写回调函数
    def borrow_money_callback(self,request, response):
        """
        借钱回调函数
        参数:request 客户端请求
             response 服务端响应
        返回值:response
        """
        self.get_logger().info("收到来自: %s 的借钱请求,目前账户内还有%d元" % (request.name, self.account))
        #根据李四借钱规则,借出去的钱不能多于自己所有钱的十分之一,不然就不借
        if request.money <= int(self.account*0.1):
            response.success = True
            response.money = request.money
            self.account = self.account - request.money
            self.get_logger().info("借钱成功,借出%d 元 ,目前账户余额%d 元" % (response.money,self.account))
        else:
            response.success = False
            response.money = 0
            self.get_logger().info("对不起兄弟,手头紧,不能借给你")
        return response
    

    这里代码其实并不复杂,先判断要借钱的金额是否满足要借出去的数量,如果满足则借,不然就不借。

    为了测试方便,我们为李四的光头账户打赏70块钱,将__init__函数中的self.account 默认账户值改为70即可

2.编写客户端端代码

客户端基本步骤:

#导入服务接口
#创建请求结果接收回调函数
#声明并创建客户端
#编写结果接收逻辑
#调用客户端发送请求
  • 添加依赖

    第一步和服务端相同,导入对应的接口,因为李四和李三是在同一个包village_li内,所以不需要再次修改package.xml

  • 导入服务接口
    #从村庄接口服务类中导入借钱服务
    from village_interfaces.srv import BorrowMoney
    
  • 创建客户端
    #在__init__函数中创建一个服务的客户端
    self.borrow_money_client_ = self.create_client(BorrowMoney, "borrow_money")
    

    创建客户端使用函数create_client该函数有两个入口参数,一个是服务接口类型,一个是服务名称。

    这里的两个参数需和服务端的完全一致,方可通信。名字不一致,会找不到对应服务,数据类型不一致会导致无法通信。

  • 编写结果接收逻辑
    def borrow_respoonse_callback(self,response):
        """
        借钱结果回调
        """
        # 打印一下信息
        result = response.result()
        if result.success == True:
            self.get_logger().info("果然是亲弟弟,借到%d,吃麻辣烫去了" % result.money)
        else:
            self.get_logger().info("害,连几块钱都不借,我还是不是他亲哥了,世态炎凉呀")
    
  • 调用客户端发送请求

    接着我们在BaiPiaoNode中编写一个函数用于创建发送的数据,并发送请求。

    def borrow_money_eat(self):
        """
        借钱吃麻辣烫函数
        """
        #打印一句话
        self.get_logger().info("找我弟借钱吃麻辣烫喽")
        #等待服务启动,每1s检查一次,如果服务没有启动,则一直循环
        while not self.borrow_money_client_.wait_for_service(1.0):
            self.get_logger().warn("我弟不在线,我再等等。")
        # 构建请求内容
        request = BorrowMoney.Request()
        #将当前节点名称作为借钱者姓名
        request.name = self.get_name()
        #借钱金额10元
        request.money = 10
        #发送异步借钱请求,借钱成功后就调用borrow_respoonse_callback()函数
        self.borrow_money_client_.call_async(request).add_done_callback(self.borrow_respoonse_callback)	
    
    • wait_for_service(1.0)用于等待服务上线,这是一种很优雅的做法,调用之前检测一下服务是否在线
    • call_async(request).add_done_callback这里是代码的核心部分,用于发送异步服务请求,并且添加了一个任务完成时的回调函数borrow_respoonse_callback

三、服务实现(C++)

剧情梗概:
张三(客户端)拿多少钱钱给王二,王二(服务端)凑够多少个章节的艳娘传奇给他

1.编写服务端代码

服务端基本步骤:

#1.导入服务接口
#2.创建服务端回调函数
#3.声明并创建服务端
#4.编写回调函数逻辑处理请求
  • 添加依赖

    第一步修改package.xml

    加入下面的代码(告诉colcon,编译之前要确保有village_interfaces存在)

    <depend>village_interfaces</depend>
    

    第二步修改和CMakeLists.txt

    ①查找库

    find_package(village_interfaces REQUIRED)
    

    ②将其和可执行文件链接起来

    ament_target_dependencies(wang2_node 
      rclcpp 
      village_interfaces
    )
    
  • 导入服务接口
    #include "village_interfaces/srv/sell_novel.hpp"
    
  • 创建服务端

    private:声明服务端

    // 声明一个服务端
    rclcpp::Service<village_interfaces::srv::SellNovel>::SharedPtr server_;
    

    在构造函数中实例化服务端

    // 实例化卖二手书的服务
    server_ = this->create_service<village_interfaces::srv::SellNovel>("sell_novel",
                                std::bind(&SingleDogNode::sell_book_callback,this,_1,_2),
                                rmw_qos_profile_services_default,
                                callback_group_service_);
    
  • 编写回调函数

    ①声明并实例化回调函数组(用以实现多线程)

    // 声明一个服务回调组
    rclcpp::CallbackGroup::SharedPtr callback_group_service_;
    
    // 实例化回调函数组
    callback_group_service_ = this->create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive);
    

    ②编写回调函数(其中引入了队列容器来存放小说队列)

    //创建一个小说章节队列
    std::queue<std::string>  novels_queue;
    
    // 声明一个回调函数,当收到买书请求时调用该函数,用于处理数据
    void sell_book_callback(const village_interfaces::srv::SellNovel::Request::SharedPtr request,
        const village_interfaces::srv::SellNovel::Response::SharedPtr response)
    {
        RCLCPP_INFO(this->get_logger(), "收到一个买书请求,一共给了%d钱",request->money);
        unsigned int novelsNum = request->money*1;  //应给小说数量,一块钱一章
    
        //判断当前书库里书的数量是否满足张三要买的数量,不够则进入等待函数
        if(novels_queue.size()<novelsNum)
        {
            RCLCPP_INFO(this->get_logger(), "当前艳娘传奇章节存量为%d:不能满足需求,开始等待",novels_queue.size());
    
            // 设置rate周期为1s,代表1s检查一次
            rclcpp::Rate loop_rate(1);
    
            //当书库里小说数量小于请求数量时一直循环
            while (novels_queue.size()<novelsNum)
            {
                //判断系统是否还在运行
                if(!rclcpp::ok())
                {
                    RCLCPP_ERROR(this->get_logger(), "程序被终止了");
                    return ;
                }
                //打印一下当前的章节数量和缺少的数量
                RCLCPP_INFO(this->get_logger(), "等待中,目前已有%d章,还差%d章",novels_queue.size(),novelsNum-novels_queue.size());
    
                //rate.sleep()让整个循环1s运行一次
                loop_rate.sleep();
            }
        }
        // 章节数量满足需求了
        RCLCPP_INFO(this->get_logger(), "当前艳娘传奇章节存量为%d:已经满足需求",novels_queue.size());
    
        //一本本把书取出来,放进请求响应对象response中
        for(unsigned int i =0 ;i<novelsNum;i++)
        {
            response->novels.push_back(novels_queue.front());
            novels_queue.pop();
        }
    }
    
    

    ③修改订阅者的回调函数(因为王2的书来源于李4发布的,所以当其发布后要将小说放入novels_queue中 novels_queue.push(msg->data)

     // 收到话题数据的回调函数
     void topic_callback(const std_msgs::msg::String::SharedPtr msg){
         // 新建一张人民币
         std_msgs::msg::UInt32 money;
         money.data = 10;
    
        // 发送人民币给李四
        pub_->publish(money);
        RCLCPP_INFO(this->get_logger(), "王二:我收到了:'%s' ,并给了李四:%d 元的稿费", msg->data.c_str(),money.data);
    
        //将小说放入novels_queue中
        novels_queue.push(msg->data);
    };
    
  • 修改main函数

    新建一个多线程执行器,添加王二节点并spin,完整代码如下:

    int main(int argc, char **argv)
    {
        rclcpp::init(argc, argv);
        /*产生一个Wang2的节点*/
        auto node = std::make_shared<SingleDogNode>("wang2");
        /* 运行节点,并检测退出信号*/
        rclcpp::executors::MultiThreadedExecutor exector;
        exector.add_node(node);
        exector.spin();
        rclcpp::shutdown();
        return 0;
    }
    

2.编写客户端端代码

客户端基本步骤:

#导入服务接口
#创建请求结果接收回调函数
#声明并创建客户端
#编写结果接收逻辑
#调用客户端发送请求
  • 添加依赖

    方法同上,不再赘述

  • 导入服务接口

    方法同上,不再赘述

  • 声明并实例化客户端
    // 创建一个客户端
    rclcpp::Client<village_interfaces::srv::SellNovel>::SharedPtr client_;
    
    //实例化客户端
    client_ = this->create_client<village_interfaces::srv::SellNovel>("sell_novel");
    
  • 编写结果接收逻辑
    //创建接收到小说的回调函数
    void novels_callback(rclcpp::Client<village_interfaces::srv::SellNovel>::SharedFuture  response)
    {
        auto result = response.get();
    
        RCLCPP_INFO(this->get_logger(), "收到%d章的小说,现在开始按章节开读", result->novels.size());
    
        for(std::string novel:result->novels)
        {
            //打印小说章节内容
            RCLCPP_INFO(this->get_logger(), "%s", novel.c_str());
        }
    
        RCLCPP_INFO(this->get_logger(), "小说读完了,好刺激,写的真不错,好期待下面的章节呀!");
    }
    
    
  • 调用客户端发送请求

    编写请求函数but_novel()

    整个函数可以分为三个部分:

    • 等待服务端上线
    • 构造请求数据
    • 发送异步请求
    void buy_novel()
    {
        RCLCPP_INFO(this->get_logger(), "买小说去喽");
    
        //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<village_interfaces::srv::SellNovel_Request>();
        //先来五块钱的看看好不好看
        request->money = 5; 
    
        //3.发送异步请求,然后等待返回,返回时调用回调函数
        client_->async_send_request(request,std::bind(&PoorManNode::novels_callback, this, _1));
    };
    

四、收获与释疑

1.mkdirtouch

mkdir 是用于创建新目录(文件夹)的命令。例如,mkdir srv 将创建一个名为 srv 的目录。

touch 是用于创建新文件或更新已有文件的访问和修改时间的命令

2.基本功能包依赖项与自定义消息和服务的依赖项:

基本功能包依赖项:

  • ament_cmake: ROS 2 构建系统工具。
  • rclcpp: ROS 2 C++ 客户端库。
  • rclpy: ROS 2 Python 客户端库。
  • std_msgs: 标准消息类型。
  • sensor_msgs: 传感器消息类型。
  • geometry_msgs: 几何消息类型。

自定义消息和服务的依赖项:

如果你要定义自定义消息和服务,需要依赖 rosidl 和相关生成器:

  • rosidl_default_generators: 默认的 ROSIDL 生成器。
  • rosidl_default_runtime: 生成的消息和服务的运行时依赖

3.为什么c++中创建节点要使用std::shared_ptr类型,python只是简单创建个节点?

std::shared_ptr作用有:

  • 共享所有权:std::shared_ptr允许多个指针共享同一个对象。当最后一个指针被销毁时,对象才会被释放。
  • 引用计数:shared_ptr内部维护一个引用计数,每次拷贝时计数加一,每次销毁时计数减一。
  • 自动管理内存:与unique_ptr类似,shared_ptr也会在超出作用域时自动释放内存。

而在python中,在Python中,内存管理是由垃圾回收器自动处理的,因此不需要像在C++中那样显式地使用智能指针(例如 std::shared_ptr)来管理对象的生命周期。Python的内存管理系统使用引用计数和垃圾回收来管理对象的生命周期,这使得开发者不需要手动管理内存

4.为什么服务端的回调函数传入的responserequestSharedPtr类型,而客户端的回调函数传入的responseSharedFuture类型?

  • 在服务文件(.srv文件)中定义的请求和响应类型本身没有指定它们是共享指针类型。这是ROS 2的实现细节,服务处理的实际代码在运行时会使用共享指针来管理这些对象。

    在服务回调函数中,Request::SharedPtrResponse::SharedPtr是ROS 2自动生成的类型定义的一部分,用于管理请求和响应对象的生命周期。

  • 当你使用async_send_request方法发送异步请求时,它会返回一个std::shared_future对象。这允许你稍后在需要的时候获取操作的结果。

5.在C++中,::/ 在包含头文件和使用命名空间时具有不同的含义和用法?

包含头文件时使用 "/

  • 当你使用 #include "village_interfaces/srv/sell_novel.hpp" 时," 表示你正在包含一个头文件,而 / 表示路径分隔符。
  • 在文件系统中,路径使用 / 分隔目录。在这里,village_interfaces/srv/sell_novel.hpp 表示头文件的相对路径,指向服务定义文件 SellNovel.srv 自动生成的 C++ 头文件。

命名空间访问时使用 ::

  • rclcpp::Client<village_interfaces::srv::SellNovel> 中,:: 用于指示命名空间的访问。rclcpp 是一个命名空间,Client 是其中的一个类,而 village_interfaces::srv::SellNovel 则是你定义的服务类型。
  • village_interfaces::srv::SellNovel 表示 village_interfaces 命名空间下的 srv 子命名空间下的 SellNovel 类型,这是由ROS 2生成的服务接口的类名。

总结

  • 使用 "/ 来包含头文件,指定文件的路径。
  • 使用 :: 来访问命名空间中的类、函数、变量等。

6.为什么有些需要使用回调函数组,有些不用?

使用回调函数组的情况:

  • sell_novels_callback_group 被创建为一个互斥(Mutually Exclusive)的回调组,意味着属于这个组的回调不会并发执行。这是为了确保在处理服务请求时,不会有其他服务请求同时执行,避免了共享资源的竞争条件。
  • sell_server 在创建时被绑定到 sell_novels_callback_group 回调组,这确保了服务回调函数 sell_novel_callback 受到回调组的管理。

如果你希望某些回调可以并发执行,你可以将它们放入另一个回调组,并指定为可以并发执行的类型(Reentrant)。但在你的例子中,服务回调函数需要确保顺序执行,因此使用了互斥回调组。

不用回调函数组的情况:

  • 简单的单线程逻辑:在这个节点中,服务客户端只涉及一个简单的异步请求和一个回调函数。没有涉及到复杂的并发操作或需要互斥的资源访问。
  • 默认行为足够:ROS 2默认的执行模型对于这种简单的节点已经足够。默认情况下,回调函数是顺序执行的,能够满足你的需求。
    口的类名。

总结

  • 使用 "/ 来包含头文件,指定文件的路径。
  • 使用 :: 来访问命名空间中的类、函数、变量等。

6.为什么有些需要使用回调函数组,有些不用?

使用回调函数组的情况:

  • sell_novels_callback_group 被创建为一个互斥(Mutually Exclusive)的回调组,意味着属于这个组的回调不会并发执行。这是为了确保在处理服务请求时,不会有其他服务请求同时执行,避免了共享资源的竞争条件。
  • sell_server 在创建时被绑定到 sell_novels_callback_group 回调组,这确保了服务回调函数 sell_novel_callback 受到回调组的管理。

如果你希望某些回调可以并发执行,你可以将它们放入另一个回调组,并指定为可以并发执行的类型(Reentrant)。但在你的例子中,服务回调函数需要确保顺序执行,因此使用了互斥回调组。

不用回调函数组的情况:

  • 简单的单线程逻辑:在这个节点中,服务客户端只涉及一个简单的异步请求和一个回调函数。没有涉及到复杂的并发操作或需要互斥的资源访问。
  • 默认行为足够:ROS 2默认的执行模型对于这种简单的节点已经足够。默认情况下,回调函数是顺序执行的,能够满足你的需求。
  • 无资源竞争问题:由于这个节点没有复杂的资源共享或多个回调函数同时执行的需求,不需要通过回调函数组来管理并发或互斥行为。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值