内容参考自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_msgsfind_package的括号中(附在其他包之后):

1
2
3
4
find_package(catkin REQUIRED COMPONENTS
# other packages are already listed here
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.actionTimerActionFeedback.msgTimerActionGoal.msgTimerActionResult.msgTimerFeedback.msgTimerGoal.msgTimerResult.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
#!/usr/bin/env python
import rospy
import time
import actionlib
from basic.msg import TimerAction, TimerGoal, TimerResult

def do_timer(goal):
start_time = time.time() # 保存当前时间
time.sleep(goal.time_to_wait.to_sec()) # 将ROS的`duration`类型转换为秒
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
rostopic list
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
#!/usr/bin/env python
import rospy
import actionlib
from basic.msg import TimerAction, TimerGoal, TimerResult

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.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
1
Time elapsed: 5.001645

实现一个更复杂的动作服务器

到目前为止,动作看起来与服务非常相似,只是在配置和启动上多了一些步骤。实际上动作与服务的主要区别在于动作的异步特性。在下面的代码中,是对服务器的代码进行一些修改后得到的,实现了终止目标、处理打断诸求和实时反债等功能。

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
#!/usr/bin/env python
import rospy
import time
import actionlib
from basic.msg import TimerAction, TimerGoal, TimerResult, TimerFeedback


def do_timer(goal):
start_time = time.time()
update_count = 0 # 统计总共发布了多少发聩信息

if goal.time_to_wait.to_sec() > 60.0: # 大于60s时终止当前目标执行
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
#! /usr/bin/env python
import rospy
import time
import actionlib
from basic.msg import TimerAction, TimerGoal, TimerResult, TimerFeedback


def 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)

# Uncomment this line to test server-side abort:
# goal.time_to_wait = rospy.Duration.from_sec(500.0)
client.send_goal(goal, feedback_cb=feedback_cb)

# Uncomment these lines to test goal preemption:
# time.sleep(3.0)
# client.cancel_goal()

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
# Uncomment these lines to test goal preemption:
# time.sleep(3.0)
# client.cancel_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
# Uncomment this line to test server-side abort:
# goal.time_to_wait = rospy.Duration.from_sec(500.0)

再次运行客户端:

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)。