# 4.9 Python服务通信实现(李三借钱) 大家好,我是小鱼。上节说完如何自定义ROS2的服务接口。 相信你已经迫不及待的想尝试一下编写代码了,让我们一起来动手,让李三成功借钱,吃上麻辣烫吧。 ## 1.如何编写一个Python服务 开始之前,我们先说一下创建ROS2服务端和客户端的基本步骤。 首先是服务端: - 创建与导入服务接口 - 创建服务端并定义服务端回调函数 - 编写回调函数处理数据并返回结果 客户端 - 导入服务接口 - 创建客户端及客户端接收服务结果的回调函数 - 在回调函数中处理服务端返回结果 ## 2.编写服务端李四代码 我们先来创建李四这边的服务端。用VsCode打开我们的`town_ws`工作区。 ### 2.1 导入服务接口 我们在上一节中自定义的服务接口这里该怎么使用呢? 需要下面两个步骤: #### 2.1.1 添加依赖 导入依赖是为了能够让我们的代码找到对应的接口。 因为`village_li`是包类型是`ament_python`这里只需要在`package.xml`中加入下面的代码即可: ``` village_interfaces ``` ![image-20210816153438400](4.9服务实现(Python)/imgs/image-20210816153438400.png) #### 2.1.2 程序中导入 程序中导入也是只需要一行代码即可完成,打开li4.py,在文件开头加入下面一行代码。 ``` #从村庄接口服务类中导入借钱服务 from village_interfaces.srv import BorrowMoney ``` ### 2.2 创建服务端并定义服务回调函数 #### 2.2.1创建服务端 接着创建一个服务,继承于`Node`之后,`Li4Node`也具备了创建一个服务的能力。在`Li4Node`的`__init__`函数中创建成员变量`borrow_server`。 ``` # 新建借钱服务 self.borrow_server = self.create_service(BorrowMoney, "borrow_money", self.borrow_money_callback) ``` 需要传入三个参数: - 服务接口类型,`BorrowMoney`,我们在2.1.2导入的 - 服务名称,`"borrow_money"`,具有唯一性,自己手打的 - 回调函数,`self.borrow_money_callback`,我们下一步定义的。 > 关于回调函数小鱼写过一篇文章:[回调函数与异步执行](https://mp.weixin.qq.com/s/BW18iCGqxlbS3KDF5rp0Aw),不理解的同学可以看一看 #### 2.2.2 定义回调函数 ```python def borrow_money_callback(self,request, response): """ 借钱回调函数 参数:request 客户端请求对象,携带着来自客户端的数据 response 服务端响应,返回服务端的处理结果 返回值:response """ return response ``` 这个函数有三个入口参数,self代表本身,这个没啥好说的,类似于c++和java里的this。 - request 是客户端请求对象,携带着来自客户端的数据。 其结构就是上一节中我们所定义的`name`和`money`组成 - response 是服务端响应,返回服务端的处理结果 其结构由`success`和`money`组成 ### 2.3编写回调函数 接下来开始正式编写回调函数,回调函数的输入是request和response,输出是我们处理后的reponse(当然也可以不处理,使用默认值) ```python 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即可 至此,服务端的代码就编写完成了,完整版代码可以点开[这个网址](https://raw.githubusercontent.com/fishros/ros2_town/master/village_li/village_li/li4.py)查看 ## 3.测试服务端代码 ### 3.1编译运行 在vscode中,使用`Ctrl+Shift+~`打开一个新的终端,在`town_ws`目录下输入: ```shell colcon build --packages-select village_li ``` ![image-20210816163422495](4.9服务实现(Python)/imgs/image-20210816163422495.png) ### 3.2启动并查看服务列表 先`source`,再`run` ``` source install/setup.bash ros2 run village_li li4_node ``` ![image-20210816163704618](4.9服务实现(Python)/imgs/image-20210816163704618.png) ### 3.3手动调用 在VsCode中使用`Ctrl+Shift+5`打开一个切分终端。然后依次输入下面的指令,查看我们的服务。 ``` ros2 service list #服务列表 ros2 service list -t #服务列表带类型 ``` 接着我们使用命令行来手动调用服务,不知道你还是否记得4.7中我们手动调用服务将两个数字相加。 这里我们手动调用服务用李三的名义来借5块钱。 ``` source install/setup.bash ros2 service call /borrow_money village_interfaces/srv/BorrowMoney "{name: 'li3', money: 5}" ``` 看返回结果success为True,money的值也变成了5,说明李三借钱成功了。 ![image-20210816164314308](4.9服务实现(Python)/imgs/image-20210816164314308.png) 再尝试借50块钱看看李四借不借。 ``` ros2 service call /borrow_money village_interfaces/srv/BorrowMoney "{name: 'li3', money: 50}" ``` 这次李四说他手头紧,不给借。返回值中的success也变成了False,money也变成了0。 ![image-20210816164511430](4.9服务实现(Python)/imgs/image-20210816164511430.png) ## 4.编写客户端代码 服务端搞定了后,我们来编写客户端李三这边的代码。 ### 4.1导入服务接口 第一步和服务端相同,导入对应的接口,因为李四和李三是在同一个包`village_li`内,所以不需要再次修改`package.xml`。 打开`li3.py`我们直接导入对应接口 ``` from village_interfaces.srv import BorrowMoney ``` ### 4.2创建客户端并定义完成时回调函数 李三继承于Node,也具备了创建客户端的能力 ``` class Li3Node(Node): #Li3Node是继承于Node ``` #### 4.2.1 创建客户端 ``` #在__init__函数中创建一个服务的客户端 self.borrow_money_client_ = self.create_client(BorrowMoney, "borrow_money") ``` 创建客户端使用函数`create_client`该函数有两个入口参数,一个是服务接口类型,一个是服务名称。 > 这里的两个参数需和服务端的完全一致,方可通信。名字不一致,会找不到对应服务,数据类型不一致会导致无法通信。 ### 4.3 发送异步请求,并编写返回结果回调函数 #### 4.3.1 编写发送请求函数 接着我们在`Li3Node中`编写一个函数用于创建发送的数据,并发送请求。 ``` 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` #### 4.3.2 编写回调函数 编写`borrow_respoonse_callback`借钱结果回调函数,该函数的只有一个入口参数`response` 我们可以从response中拿到数据并进行处理,该函数完整代码如下: ``` 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("害,连几块钱都不借,我还是不是他亲哥了,世态炎凉呀") ``` #### 4.2.3 修改main函数调用发送请求函数 因为发送请求的函数是李三的成员函数,所以我们直接调用李三来发送请求即可,可以将main函数做如下修改(其实只增加了一行代码而已)。 ``` def main(args=None): """ ros2运行该节点的入口函数,可配置函数名称 """ rclpy.init(args=args) # 初始化rclpy node = Li3Node() # 新建一个节点 node.borrow_money_eat() #增加一行,李三借钱 rclpy.spin(node) # 保持节点运行,检测是否收到退出指令(Ctrl+C) rclpy.shutdown() # rcl关闭 ``` 编写好客户端之后,我们就可以做整体的测试了,但要记得编译程序哦。完整的li3.py代码可以[访问这里](https://raw.githubusercontent.com/fishros/ros2_town/master/village_li/village_li/li3.py)获取 > 除了使用`call_async(request)`异步调用,还有一种同步调用的方式,但小鱼并不推荐,原因这里小鱼`mark@鱼香ROS`一下,后面在公众号中单独写一篇文章介绍。 ## 5.整体测试 嘀嘀嘀,终于可以开始最终的测试了。 ### 5.1编译功能包 在vscode中,使用`Ctrl+Shift+~`打开一个新的终端,在`town_ws`目录下输入: ```shell colcon build --packages-select village_li ``` ![image-20210816163422495](4.9服务实现(Python)/imgs/image-20210816163422495.png) ### 5.2运行客户端李三程序 #### 5.2.1 source ``` source install/setup.bash ``` #### 5.2.2 运行客户端代码 ``` ros2 run village_li li3_node ``` ![image-20210817104713043](4.9服务实现(Python)/imgs/image-20210817104713043.png) ### 5.3运行服务端代码 #### 5.3.1 切分终端并source vscode中使用`Ctrl+Shift+5`重新切分出一个终端,然后source ``` source install/setup.bash ``` #### 5.3.2 运行服务端李四程序 ``` ros2 run village_li li4_node ``` #### 5.3.3 运行结果 ![image-20210817105329996](4.9服务实现(Python)/imgs/image-20210817105329996.png) 从图片中可以看到,李三借钱失败了,虽然小鱼已经将李四账户里的钱的默认值从0元修改成了80元,但80*0.1=8<10,依然不能借给李三十块钱,那为了李三能够吃上麻辣烫,我们可以帮助李四赚钱——让王二过来进行知识付费。 ### 5.4运行王二过来知识付费 同样的再切分出一个终端,然后source运行王二节点。 ``` source install/setup.bash ros2 run village_wang wang2_node ``` 重新运行李三节点,点击李三运行的终端,先输入`Ctrl+C`使其退出,再重新运行节点。 ``` ros2 run village_li li3_node ``` ![image-20210817110416711](4.9服务实现(Python)/imgs/image-20210817110416711.png) 可以看到,此时李四账户里已经有了六百多块了,很轻松的借给了李三10块钱,这多亏了王二的知识付费。 ## 6.结束 至此,我们帮助李三成功借钱,吃上了麻辣烫,下一步就是编写C++程序,努力帮助张三看上二手书。 如果还有不明白的地方,欢迎加入鱼群交流。