ROS2 Humble 笔记(八)动作 action

这篇博客是 B 站《古月·ROS2入门21讲》的第十一个视频的图文记录,主要介绍了动作 action 的使用和实现示例,虽然 action 底层是以 service + topic 方式实现的,但对于上层使用者而言其 action 的封装可以极大减轻状态量的管理琐事。原始视频链接如下:


1. 动作 action 的意义

在之前的博客中有介绍到 话题服务 这两个概念,前者其实不关系订阅者的状态,后者虽然会等待服务提供者返回,但也不关系中间状态。

然而,在实际应用中经常会出现需要紧盯中间态的情况,并在必要的时候将其打断,例如小车运动过程中可以实时返回自己走了多久等。虽然你也可以通过话题和服务组合的形式实现这样的功能,但这样 打破了完整性,即你需要自己手动管理话题数据什么时候能作为决策依据并生效,还需要预估服务大概执行到什么状态,当后期机器人状态特别多的时候你 可能会同时维护十几个话题,这种状态机的复杂度是指数增加的。

action 就是为了应对上面情况而产生的,你只需要将有用的中间态作为 feedback 返回给调用者进行决策,而不需要将所有状态都开放出来。


2. 小海龟示例

2.1 仿真器与键盘控制

先打开一个终端启动小海龟仿真器:

bash 复制代码
$ ros2 run turtlesim turtlesim_node

然后再开一个终端控制小海龟进行运动,在该终端上有 G|B|V|C|E|R|T 这几个选项分别表示让小海龟转向固定的位置,你可以在键盘上敲击这几个键,同时还有 F 键表示在转动过程中打断旋转,这里就是通过 action 实现的:

bash 复制代码
$ ros2 run turtlesim turtle_teleop_key

2.2 终端形式调用

使用 list 命令可以查看当前有哪些 action 可以使用,当前仅有一个 /turtle1/rotate_absolute 动作可用:

bash 复制代码
$ ros2 action list

使用 info 命令可以查看该 action 的详细信息:

bash 复制代码
$ ros2 action info /turtle1/rotate_absolute

使用 send_goal 命令可以向海龟发送一个 action 请求,注意下面这个请求中 theta 的单位是 弧度,在发送请求可以可以观察到小海龟执行了原地旋转:

bash 复制代码
$ ros2 action send_goal /turtle1/rotate_absolute turtlesim/action/RotateAbsolute "{theta: 3.14}"

可以通过 --feedback 命令将小海龟的中间状态打印出来:

bash 复制代码
$ ros2 action send_goal /turtle1/rotate_absolute turtlesim/action/RotateAbsolute "{theta: 0.0}" --feed
back

3. 动作 action 实现

3.1 Server & Client

这两个节点在 learning_action 功能包下。此处的 Server 和 Client 和之前服务的概念基本一致,唯一不同的是这里的 Server 和 Client 是以 action 作为媒介交互的。

打开一个终端启动 Server,启动后同样会阻塞等待 Client 请求:

bash 复制代码
$ ros2 run learning_action action_move_server

再开一个终端启动 Client,启动后会向 Server 发送一个动作请求,并在执行过程中有反馈:

bash 复制代码
$ ros2 run learning_action action_move_client

3.2 action 接口定义

learning_interface 功能包中的 action 文件夹下定义了接口,具体位置在 src/ros2_21_tutorials/learning_interface/action/MoveCircle.action

  • MoveCircle.action
python 复制代码
# Goal
bool enable     # 定义动作的目标,表示动作开始的指令
---
# Result
bool finish     # 定义动作的结果,表示是否成功执行
---
# Feedback
int32 state     # 定义动作的反馈,表示当前执行到的位置

action 和 srv、msg 一样,需要在 CMakeLists.txt 文件中添加:

cmake 复制代码
rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/ObjectPosition.msg"
  "srv/AddTwoInts.srv"
  "srv/GetObjectPosition.srv"
  "action/MoveCircle.action"
 )

3.3 Server 端实现

该节点在 learning_action 功能包中,具体位置为 src/ros2_21_tutorials/learning_action/learning_action/action_move_server.py

  • action_move_server.py
python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import time

import rclpy                                      # ROS2 Python接口库
from rclpy.node   import Node                     # ROS2 节点类
from rclpy.action import ActionServer             # ROS2 动作服务器类
from learning_interface.action import MoveCircle  # 自定义的圆周运动接口

class MoveCircleActionServer(Node):
    def __init__(self, name):
        super().__init__(name)                   # ROS2节点父类初始化
        self._action_server = ActionServer(      # 创建动作服务器(接口类型、动作名、回调函数)
            self,
            MoveCircle,
            'move_circle',
            self.execute_callback)

    def execute_callback(self, goal_handle):            # 执行收到动作目标之后的处理函数
        self.get_logger().info('Moving circle...')
        feedback_msg = MoveCircle.Feedback()            # 创建一个动作反馈信息的消息

        for i in range(0, 360, 30):                     # 从0到360度,执行圆周运动,并周期反馈信息
            feedback_msg.state = i                      # 创建反馈信息,表示当前执行到的角度
            self.get_logger().info('Publishing feedback: %d' % feedback_msg.state)
            goal_handle.publish_feedback(feedback_msg)  # 发布反馈信息
            time.sleep(0.5)

        goal_handle.succeed()                           # 动作执行成功
        result = MoveCircle.Result()                    # 创建结果消息
        result.finish = True                            
        return result                                   # 反馈最终动作执行的结果

def main(args=None):                                       # ROS2节点主入口main函数
    rclpy.init(args=args)                                  # ROS2 Python接口初始化
    node = MoveCircleActionServer("action_move_server")    # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                                       # 循环等待ROS2退出
    node.destroy_node()                                    # 销毁节点对象
    rclpy.shutdown()                                       # 关闭ROS2 Python接口

代码中核心部分为 execute_callbackActionServer 定义:


3.4 Client 端实现

该节点在 learning_action 功能包中,具体位置为 src/ros2_21_tutorials/learning_action/learning_action/action_move_client.py

  • action_move_client.py
python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import rclpy                                      # ROS2 Python接口库
from rclpy.node   import Node                     # ROS2 节点类
from rclpy.action import ActionClient             # ROS2 动作客户端类

from learning_interface.action import MoveCircle  # 自定义的圆周运动接口

class MoveCircleActionClient(Node):
    def __init__(self, name):
        super().__init__(name)                   # ROS2节点父类初始化
        self._action_client = ActionClient(      # 创建动作客户端(接口类型、动作名)
            self, MoveCircle, 'move_circle') 

    def send_goal(self, enable):                 # 创建一个发送动作目标的函数
        goal_msg = MoveCircle.Goal()             # 创建一个动作目标的消息
        goal_msg.enable = enable                 # 设置动作目标为使能,希望机器人开始运动

        self._action_client.wait_for_server()    # 等待动作的服务器端启动
        self._send_goal_future = self._action_client.send_goal_async(   # 异步方式发送动作的目标
            goal_msg,                                                   # 动作目标
            feedback_callback=self.feedback_callback)                   # 处理周期反馈消息的回调函数
                          
        self._send_goal_future.add_done_callback(self.goal_response_callback) # 设置一个服务器收到目标之后反馈时的回调函数

    def goal_response_callback(self, future):           # 创建一个服务器收到目标之后反馈时的回调函数
        goal_handle = future.result()                   # 接收动作的结果
        if not goal_handle.accepted:                    # 如果动作被拒绝执行
            self.get_logger().info('Goal rejected :(')
            return

        self.get_logger().info('Goal accepted :)')                            # 动作被顺利执行

        self._get_result_future = goal_handle.get_result_async()              # 异步获取动作最终执行的结果反馈
        self._get_result_future.add_done_callback(self.get_result_callback)   # 设置一个收到最终结果的回调函数 

    def get_result_callback(self, future):                                    # 创建一个收到最终结果的回调函数
        result = future.result().result                                       # 读取动作执行的结果
        self.get_logger().info('Result: {%d}' % result.finish)                # 日志输出执行结果

    def feedback_callback(self, feedback_msg):                                # 创建处理周期反馈消息的回调函数
        feedback = feedback_msg.feedback                                      # 读取反馈的数据
        self.get_logger().info('Received feedback: {%d}' % feedback.state) 

def main(args=None):                                       # ROS2节点主入口main函数
    rclpy.init(args=args)                                  # ROS2 Python接口初始化
    node = MoveCircleActionClient("action_move_client")    # 创建ROS2节点对象并进行初始化
    node.send_goal(True)                                   # 发送动作目标
    rclpy.spin(node)                                       # 循环等待ROS2退出
    node.destroy_node()                                    # 销毁节点对象
    rclpy.shutdown()                                       # 关闭ROS2 Python接口

在 Client 中 feedback_callbackget_result_callback 都是异步执行的,因为不知道 Server 端什么时候会完成任务,因此在等待 feedback 的时候 Client 可以同步执行其他动作:

相关推荐
三品吉他手会点灯2 小时前
STM32F103学习笔记-16-RCC(第3节)-使用HSE配置系统时钟并使用MCO输出监控系统时钟
c语言·笔记·stm32·单片机·嵌入式硬件·学习
摇滚侠2 小时前
Vue 项目实战《尚医通》,医院详情菜单与子路由,笔记17
前端·vue.js·笔记
CarmenHu3 小时前
IBM RAG挑战赛冠军方案学习笔记
笔记·学习
赶飞机偏偏下雨3 小时前
【MySQL笔记】索引 (非常重要)
笔记
星星20253 小时前
MBSE与数字孪生:五大行业案例
笔记
yun68539925 小时前
读书之《架构师的自我修炼》_个人笔记
笔记
QT 小鲜肉5 小时前
【个人成长笔记】将Try Ubuntu里面配置好的文件系统克隆在U盘上(创建一个带有持久化功能的Ubuntu Live USB系统)
linux·开发语言·数据库·笔记·ubuntu
kyle~5 小时前
数学基础---四元数
人工智能·数学·机器人·旋转
勿忘初心915 小时前
游戏手柄遥控越疆协作机器人[三]
游戏·机器人