机器人是一个复杂的智能系统,并不仅仅是键盘遥控运动、识别某个目标这么简单,我们需要实现的是送餐、送货、分拣等满足具体场景需求的机器人。
在这些应用功能的实现中,另外一种ROS通信机制也会被常常用到——那就是动作。从这个名字上就可以很好理解这个概念的含义,这种通信机制的目的就是便于对机器人某一完整行为的流程进行管理。
一、通信模型
举个例子,比如我们想让机器人转个圈,这肯定不是一下就可以完成的,机器人得一点一点旋转,直到360度才能结束,假设机器人并不在我们眼前,发出指令后,我们根本不知道机器人到底有没有开始转圈,转到哪里了?
OK,现在我们需要的是一个反馈,比如每隔1s,告诉我们当前转到多少度了,10度、20度、30度,一段时间之后,到了360度,再发送一个信息,表示动作执行完成。
这样一个需要执行一段时间的行为,使用动作的通信机制就更为合适,就像装了一个进度条,我们可以随时把控进度,如果运动过程当中,我们还可以随时发送一个取消运动的命令。
1.1 客户端/服务器模型
动作和服务类似,使用的也是客户端和服务器模型,客户端发送动作的目标,想让机器人干什么,服务器端执行动作过程, 控制机器人达到运动的目标,同时周期反馈动作执行过程中的状态。
客户端发送一个运动的目标,想让机器人动起来,服务器端收到之后,就开始控制机器人运动,一边运动,一边反馈当前的状态;
- 如果是一个导航动作,这个反馈可能是当前所处的坐标;
- 如果是机械臂抓取,这个反馈可能又是机械臂的实时姿态;
当运动执行结束后,服务器再反馈一个动作结束的信息,整个通信过程就此结束。
1.2 一对多通信
和服务一样,动作通信中的客户端可以有多个,大家都可以发送运动命令,但是服务器端只能有一个,毕竟只有一个机器人,先执行完成一个动作,才能执行下一个动作。
1.3 同步通信
既然有反馈,那动作也是一种同步通信机制,之前我们也介绍过,动作过程中的数据通信接口,使用.action文件进行定义。
1.4 由服务和话题合成
我们再仔细看下上边的动图,是不是还会发现一个隐藏的秘密。动作的三个通信模块,竟然有两个是服务,一个是话题;
- 当客户端发送运动目标时,使用的是服务的请求调用,服务器端也会反馈一个应带,表示收到命令;
- 动作的反馈过程,其实就是一个话题的周期发布,服务器端是发布者,客户端是订阅者。
没错,动作是一种应用层的通信机制,其底层就是基于话题和服务来实现的。
二、动作案例
2.1 小海龟的动作
我们使用小海龟的案例加深对动作概念的理解。进入桌面系统,启动第一个终端,运行小海龟仿真器;- pi@NanoPC-T6:~/dev_ws$ ros2 run turtlesim turtlesim_node
复制代码 该指令将启动一个蓝色背景的海龟仿真器;
启动第二个终端,运行如下指令:- pi@NanoPC-T6:~/dev_ws$ ros2 run turtlesim turtle_teleop_key
复制代码 该指令将启动一个键盘控制节点,在该终端中点击键盘上的“上下左右”按键,就可以控制小海龟运动啦。
接下来使用action命令控制小海龟的动作,可以让海龟运动到某一指定的姿态:- pi@NanoPC-T6:~/Desktop$ ros2 action send_goal /turtle1/rotate_absolute turtlesim/action/RotateAbsolute "{theta: -1.57}"
- Waiting for an action server to become available...
- Sending goal:
- theta: -1.57
- Goal accepted with ID: 08d36694d66f4977b1bbf0f2bdd93d69
- Result:
- delta: 1.5360000133514404
- Goal finished with status: SUCCEEDED
复制代码 如果需要反馈结果,可以执行;- pi@NanoPC-T6:~/Desktop$ ros2 action send_goal /turtle1/rotate_absolute turtlesim/action/RotateAbsolute "{theta: 0}" --feedback
- Waiting for an action server to become available...
- Sending goal:
- theta: 0.0
- Goal accepted with ID: 2bbb1d8a0a7d474a9b5a41c2c2f0cd03
- Feedback:
- remaining: 1.5520000457763672
- Feedback:
- remaining: 1.5360000133514404
- Feedback:
- remaining: 1.5199999809265137
- Feedback:
- remaining: 1.503999948501587
- ......
复制代码 2.2 机器人画圆
如何通过代码来实现动作的编程呢?动作虽然是基于话题和服务实现的,但在实际使用中,并不会直接使用话题和服务的编程方法,而是有一套针对动作特性封装好的编程接口,接下来,我们就来看看, 动作到底该如何实现。
假设我们有一个机器人,我们希望通过动作的通信方法,让机器人转个圈,请编程实现动作通信中,客户端和服务器端的实现过程。
我们首先创建my_learning_action的Python版本的功能包;- pi@NanoPC-T6:~/dev_ws$ cd src
- pi@NanoPC-T6:~/dev_ws/src$ ros2 pkg create --build-type ament_python my_learning_action
复制代码 修改功能包my_learning_action依赖package.xml;- <depend>my_learning_interface</depend>
复制代码 2.2.1 接口定义
我们使用的动作并不是ROS中的标准定义,我们通过MoveCircle.action进行自定义。
我们在<depend>my_learning_interface</depend>文件夹下创建子文件夹action,接着新建文件MoveCircle.action;- bool enable # 定义动作的目标,表示动作开始的指令
- ---
- bool finish # 定义动作的结果,表示是否成功执行
- ---
- int32 state # 定义动作的反馈,表示当前执行到的位置
复制代码 动作由三个部分组成:
- 第一块是动作的目标:enable为true时,表示开始运动;
- 第二块是动作的执行结果:finish为true,表示动作执行完成;
- 第三块是动作的周期反馈:表示当前机器人旋转到的角度。
完成定义后,还需要在功能包的CMakeLists.txt中配置编译选项,让编译器在编译过程中,根据接口定义,自动生成不同语言的代码:- ...
- find_package(rosidl_default_generators REQUIRED)
- rosidl_generate_interfaces(${PROJECT_NAME}
- "action/MoveCircle.action"
- )
- ...
复制代码 编译程序:- pi@NanoPC-T6:~/dev_ws$ colcon build --paths src/<depend>my_learning_interface</depend>
复制代码 2.2.2 通信模型
通信模型如下图所示,客户端发送给一个动作目标,服务器控制机器人开始运动,并周期反馈,结束后反馈结束信息。
思路理清楚,接下来开始写代码。相比之前话题和服务的程序,动作通信的例程相对较长,我们一起来运行并分析一下。
2.2.3 服务端
打开my_learning_action功能包,在my_learning_action文件夹下创建action_move_server.py;- """ROS2动作示例-负责执行圆周运动动作的服务端@author: zy@since : 2025/12/14"""import timeimport rclpy # ROS2 Python接口库from rclpy.node import Node # ROS2 节点类from rclpy.action import ActionServer # ROS2 动作服务器类from <depend>my_learning_interface</depend>.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接口
复制代码 完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:- entry_points={
- 'console_scripts': [
- 'action_move_server = my_learning_action.action_move_server:main',
- ],
- },
复制代码 2.2.4 客户端
在my_learning_action文件夹下创建action_move_client.py;- """ROS2动作示例-请求执行圆周运动动作的客户端@author: zy@since : 2025/12/14"""import rclpy # ROS2 Python接口库from rclpy.node import Node # ROS2 节点类from rclpy.action import ActionClient # ROS2 动作客户端类from <depend>my_learning_interface</depend>.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接口
复制代码 完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:- entry_points={
- 'console_scripts': [
- 'action_move_client = my_learning_action.action_move_client:main',
- 'action_move_server = my_learning_action.action_move_server:main',
- ],
- },
复制代码 2.2.5 编译运行
编译程序:- pi@NanoPC-T6:~/dev_ws$ colcon build --paths src/my_learning_action
复制代码 启动第一个终端,启动动作示例的服务端;- pi@NanoPC-T6:~/dev_ws$ ros2 run my_learning_action action_move_server
- [INFO] [1765726344.703410263] [action_move_server]: Moving circle...
- [INFO] [1765726344.704012816] [action_move_server]: Publishing feedback: 0
- [INFO] [1765726345.207813520] [action_move_server]: Publishing feedback: 30
- [INFO] [1765726345.712013457] [action_move_server]: Publishing feedback: 60
- [INFO] [1765726346.216313390] [action_move_server]: Publishing feedback: 90
- [INFO] [1765726346.720621950] [action_move_server]: Publishing feedback: 120
- [INFO] [1765726347.223664991] [action_move_server]: Publishing feedback: 150
- [INFO] [1765726347.727581949] [action_move_server]: Publishing feedback: 180
- [INFO] [1765726348.231748272] [action_move_server]: Publishing feedback: 210
- [INFO] [1765726348.735497200] [action_move_server]: Publishing feedback: 240
- [INFO] [1765726349.239607404] [action_move_server]: Publishing feedback: 270
- [INFO] [1765726349.743132014] [action_move_server]: Publishing feedback: 300
- [INFO] [1765726350.247313968] [action_move_server]: Publishing feedback: 330
复制代码 启动第二个终端,启动动作示例的客户端;- pi@NanoPC-T6:~/dev_ws$ ros2 run my_learning_action action_move_client
- [INFO] [1765726344.695723183] [action_move_client]: Goal accepted :)
- [INFO] [1765726344.705272754] [action_move_client]: Received feedback: {0}
- [INFO] [1765726345.212856772] [action_move_client]: Received feedback: {30}
- [INFO] [1765726345.716884342] [action_move_client]: Received feedback: {60}
- [INFO] [1765726346.221326019] [action_move_client]: Received feedback: {90}
- [INFO] [1765726346.725441505] [action_move_client]: Received feedback: {120}
- [INFO] [1765726347.227005493] [action_move_client]: Received feedback: {150}
- [INFO] [1765726347.732291176] [action_move_client]: Received feedback: {180}
- [INFO] [1765726348.236114807] [action_move_client]: Received feedback: {210}
- [INFO] [1765726348.740333462] [action_move_client]: Received feedback: {240}
- [INFO] [1765726349.244567077] [action_move_client]: Received feedback: {270}
- [INFO] [1765726349.747922695] [action_move_client]: Received feedback: {300}
- [INFO] [1765726350.251839829] [action_move_client]: Received feedback: {330}
- [INFO] [1765726350.757741242] [action_move_client]: Result: {1}
复制代码 终端中,我们可以看到客户端发送动作目标之后,服务器端开始模拟机器人运动,每30度发送一次反馈信息,最终完成运动,并反馈结束运动的信息。
2.3 动作命令行操作
查看动作列表:- pi@NanoPC-T6:~/dev_ws$ ros2 action list
- /move_circle
复制代码 查看动作信息:- pi@NanoPC-T6:~/dev_ws$ ros2 action info /move_circle
- Action: /move_circle
- Action clients: 1
- /action_move_client
- Action servers: 1
- /action_move_server
复制代码 其中:
- 动作名称:/move_circle;
- 动作客户端:1个 (/action_move_client);
- 动作服务器:1个 (/action_move_server)。
查看动作接口的详细定义:- pi@NanoPC-T6:~/dev_ws$ ros2 interface show <depend>my_learning_interface</depend>/action/MoveCirclebool enable # 定义动作的目标,表示动作开始的指令
- ---
- bool finish # 定义动作的结果,表示是否成功执行
- ---
- int32 state # 定义动作的反馈,表示当前执行到的位置
复制代码 发送动作请求:- # ros2 action send_goal pi@NanoPC-T6:~/dev_ws$ ros2 action send_goal /move_circle <depend>my_learning_interface</depend>/action/MoveCircle "{enable: true}" --feedbackWaiting for an action server to become available...Sending goal: enable: trueGoal accepted with ID: 8b9cfe2a456e450f90ed56ba8b5c82ddFeedback: state: 0Feedback: state: 30Feedback: state: 60Feedback: state: 90Feedback: state: 120Feedback: state: 150Feedback: state: 180Feedback: state: 210Feedback: state: 240Feedback: state: 270Feedback: state: 300Feedback: state: 330Result: finish: trueGoal finished with status: SUCCEEDED
复制代码 参考文章
[1] 古月居ROS2入门教程学习笔记
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |