内容参考自Programming Robots with ROS by Morgan Quiley,
Brian Gerkey, and William D. Smart (O'Reily). Copyright 2015 Morgan
Quiley, Brian Gerkey, and William D. Smart, 978-1-4493-2389-9
ROS的动作机制非常适合作为时间不确定,目标导向型的操作的接口,比如goto_position。服务机制是同步的,而动作机制则是异步的。与服务的诸求/响应结构类似,动作使用目标来启动一次操作,在操作完成后会发送一个结果。在此基础上,动作引入了反馈来提供操作的进度信息,
还支持取消当前进行中的操作。从原理上看,动作使用话题实现,其本质上相当于一个规定了一系列话题(目标、结果、反馈等)的组合使用方法的高层协议。
改用动作接口实现goto_position,整个控制流程就会变成这样:首先还是发送一个目标,然后就可以转去做其他工作了。在操作的执行过程中,会周期性收到执行进度的消息(已经移动的距离、预计完成时间等),直到操作完成,收到最终的结果
(顺利完成或是提前终止)。而且,如果突然来了更重要的任务,可以随时取消当前操作,并开始一个新的操作。
动作的定义
下面将定义一个行为类似定时器的动作。这个定时器会进行倒计时,并在倒计时停止时发出信号。在计数过程中,它会定期告诉我们剩余的时间。计数结束后,它会告诉我们总共的计数时长。
Timer.action
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # This is an action definition file, which has three parts: the goal, the # result, and the feedback. # # Part 1: the goal, to be sent by the client # # The amount of time we want to wait duration time_to_wait --- # Part 2: the result, to be sent by the server upon_completion # # How much time we waited duration time_elapsed # How many updates we provided along the way uint32 updates_sent --- # Part 3: the feedback, to be sent periodically by the server during execution. # # The amount of time that has elapsed from the start duration time_elapsed # The amount of time remaining until we're done duration time_remaining
就像服务定义文件一样,我们用三个短横线表示不同定义部分的分隔,只不过服务定义文件只有两个部分(请求和响应),而动作定义文件有三个部分(目标、结果和反馈)。
动作定义文件Timer.action 应放在ROS包的action 目录下。
下一步,我们要在CMakeLists.txt 中添加一些内容。首先,添加actionlib_msgs至find_package的括号中(附在其他包之后):
1 2 3 4 find_package (catkin REQUIRED COMPONENTS actionlib_msgs )
接着,在add_action_files中告知catkin我们要编译哪些动作文件。
1 2 3 4 add_action_files( DIRECTORY action FILES Timer.action )
确保generate_messages中列出了所有消息依赖,此外,我们还要显式地列出actionlib_msgs以保证动作能被正确编译。
1 2 3 4 5 generate_messages( DEPENDENCIES actionlib_msgs std_msgs )
最后,在catkin_package添加actionlib_msgs依赖。
1 2 3 4 catkin_package( CATKIN_DEPENDS actionlib_msgs )
另外,在packages.xml 中添加:
1 2 <build_depend > actionlib_msgs</build_depend > <exec_depend > actionlib_msgs</exec_depend >
上述所有工作到位后,在catkin工作区的顶层目录运行catkin_make。动作被正确编译后会生成一些消息文件,包括Timer.action、TimerActionFeedback.msg、TimerActionGoal.msg、TimerActionResult.msg、TimerFeedback.msg、TimerGoal.msg和TimerResult.msg。这些消息文件就会被用于实现动作的client/server
协议。最终,消息文件会被转化为相应的类定义。
实现一个基本的动作服务器
代码实现
simple_action_server.py :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 import rospyimport timeimport actionlibfrom basic.msg import TimerAction, TimerGoal, TimerResultdef do_timer (goal ): start_time = time.time() time.sleep(goal.time_to_wait.to_sec()) result = TimerResult() result.time_elapsed = rospy.Duration.from_sec(time.time() - start_time) result.start() server.set_succeeded(result) rospy.init_node('timer_action_server' ) server = actionlib.SimpleActionServer('timer' , TimerAction, do_timer, False ) server.start() rospy.spin()
1 2 3 4 rospy.init_node('timer_action_server' ) server = actionlib.SimpleActionServer('timer' , TimerAction, do_timer, False ) server.start() rospy.spin()
在上面的代码中,我们像往常一样对节点进行了命名和初始化,然后创建了一个
SimpleActionServer。构造函数的第一个参数为动作服务器的名称,这个名称会成为其所有子话题的名字空闻。第二个参数为动作服务器的类型,对于我们来说是TimerAction。第三个参数为日标的回调函数。最后,通过传递False参数来关闭动作服务器的自动启动功能。完成上述构造后,我们显式调用start()来启动动作服务器,并进入ROS的spin()循环中,等待目标的到来。
功能检查
启动roscore,然后运行动作服务器:
1 rosrun basic simple_action_server.py
先看看相应的话题:
1 2 3 4 5 6 7 /rosout /rosout_agg /timer/cancel /timer/feedback /timer/goal /timer/result /timer/status
使用rostopic看看timer/goal话题:
1 rostopic info timer/goal
1 2 3 4 5 6 7 Type: basic/TimerActionGoal Publishers: None Subscribers: * /timer_action_server (http://ubuntu:35747/)
使用rosmsg进一步看看TimerActionGoal:
1 romsg show TimerActionGoal
1 2 3 4 5 6 7 8 9 10 11 [basic/TimerActionGoal]: std_msgs/Header header uint32 seq time stamp string frame_id actionlib_msgs/GoalID goal_id time stamp string id basic/TimerGoal goal duration time_to_wait
动作的使用
代码实现
simple_action_client.py :
1 2 3 4 5 6 7 8 9 10 11 12 13 import rospyimport actionlibfrom basic.msg import TimerAction, TimerGoal, TimerResultrospy.init_node('timer_action_client' ) client = actionlib.SimpleActionClient('timer' , TimerAction) client.wait_for_server() goal = TimerGoal() goal.time_to_wait = rospy.Duration.from_sec(5.0 ) client.sent_goal(goal) client.wait_for_result() print ('Time elapsed: %f' % (client.get_result().time_elapsed.to_sec()))
功能检查
确保roscore和动作服务器均已启动,然后运行客户端:
1 rosrun basic simple_action_client.py
实现一个更复杂的动作服务器
到目前为止,动作看起来与服务非常相似,只是在配置和启动上多了一些步骤。实际上动作与服务的主要区别在于动作的异步特性。在下面的代码中,是对服务器的代码进行一些修改后得到的,实现了终止目标、处理打断诸求和实时反债等功能。
fancy_action_sever.py :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 import rospyimport timeimport actionlibfrom basic.msg import TimerAction, TimerGoal, TimerResult, TimerFeedbackdef do_timer (goal ): start_time = time.time() update_count = 0 if goal.time_to_wait.to_sec() > 60.0 : result = TimerResult() result.time_elapsed = rospy.Duration.from_sec(time.time() - start_time) result.updates_sent = update_count server.set_aborted(result, "Timer aborted due to too-long wait" ) return while (time.time() - start_time) < goal.time_to_wait.to_sec(): if server.is_preempt_requested(): result = TimerResult() result.time_elapsed = rospy.Duration.from_sec(time.time() - start_time) result.updates_sent = update_count server.set_preempted(result, "Timer preempted" ) return feedback= TimerFeedback() feedback.time_elapsed = rospy.Duration.from_sec(time.time() - start_time) feedback.time_remaining = goal.time_to_wait - feedback.time_elapsed server.publish_feedback(feedback) update_count += 1 time.sleep(1.0 ) result = TimerResult() result.time_elapsed = rospy.Duration.from_sec(time.time() - start_time) result.updates_sent = update_count server.set_succeeded(result, "Timer completed successfully" ) rospy.init_node('timer_action_server' ) server = actionlib.SimpleActionServer('timer' , TimerAction, do_timer, False ) server.start() rospy.spin()
使用更复杂的动作
代码实现
fancy_action_client.py :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 import rospyimport timeimport actionlibfrom basic.msg import TimerAction, TimerGoal, TimerResult, TimerFeedbackdef feedback_cb (feedback ): print ('[Feedback] Time elapsed: %f' % (feedback.time_elapsed.to_sec())) print ('[Feedback] Time remaining: %f' %(feedback.time_remaining.to_sec())) rospy.init_node('timer_action_client' ) client = actionlib. SimpleActionClient('timer' , TimerAction) client.wait_for_server() goal = TimerGoal() goal.time_to_wait = rospy.Duration.from_sec(5.0 ) client.send_goal(goal, feedback_cb=feedback_cb) client.wait_for_result() print ('[Result] State: %d' % (client.get_state()))print ('[Result] Status: %s' % (client.get_goal_status_text()))print ('[Result] Time elapsed: %f' % (client.get_result().time_elapsed.to_sec()))print ('[Result] Updates sent: %d' % (client.get_result().updates_sent))
功能检查
先启动roscore,然后运行server。
在另一个终端内,运行客户端:
1 rosrun basic fancy_action_client.py
1 2 3 4 5 6 7 8 9 10 11 12 13 14 [Feedback] Time elapsed: 0.000031 [Feedback] Time remaining: 4.999969 [Feedback] Time elapsed: 1.002780 [Feedback] Time remaining: 3.997220 [Feedback] Time elapsed: 2.005251 [Feedback] Time remaining: 2.994749 [Feedback] Time elapsed: 3.008076 [Feedback] Time remaining: 1.991924 [Feedback] Time elapsed: 4.010132 [Feedback] Time remaining: 0.989868 [Result] State: 3 [Result] Status: Timer completed successfully [Result] Time elapsed: 5.012117 [Result] Updates sent: 5
现在试试中断一个执行中的目标。在客户端代码的send_goal()调用后对下面两行解除注释,这样会让客户端在短暫的休眠后中断当前的目标。
再次运行客户端:
1 2 3 4 5 6 7 8 9 10 [Feedback] Time elapsed: 0.000051 [Feedback] Time remaining: 4.999949 [Feedback] Time elapsed: 1.002510 [Feedback] Time remaining: 3.997490 [Feedback] Time elapsed: 2.004177 [Feedback] Time remaining: 2.995823 [Result] State: 2 [Result] Status: Timer preempted [Result] Time elapsed: 3.006934 [Result] Updates sent: 3
表现出的行为与预期吻合:服务器首先执行最开始的目标,并定期进行反馈,直到客户端发出了终止请求。发出终止请求后,客户端很快就收到了服务端发来的结果,表明上一次执行被中断
(PREEMPTED=2)。
现在来引发一个服务端的主动终止。在客户端代码中,对下面的代码解除注释将等待时间从5s改为500s。
再次运行客户端:
1 2 3 4 [Result] State: 4 [Result] Status: Timer aborted due to too-long wait [Result] Time elapsed: 0.000015 [Result] Updates sent: 0
就像我们之前预期的那样,服务器立即主动终止了目标的执行(ABORTED=4)。