1
2 import roslib; roslib.load_manifest('smach_ros')
3 import rospy
4
5 import threading
6 import traceback
7 import copy
8
9 from actionlib.simple_action_client import SimpleActionClient, GoalStatus
10
11 import smach
12 from smach.state import *
13
14 __all__ = ['SimpleActionState']
15
17 """Simple action client state.
18
19 Use this class to represent an actionlib as a state in a state machine.
20 """
21
22
23 WAITING_FOR_SERVER = 0
24 INACTIVE = 1
25 ACTIVE = 2
26 PREEMPTING = 3
27 COMPLETED = 4
28
29 - def __init__(self,
30
31 action_name,
32 action_spec,
33
34 goal = None,
35 goal_key = None,
36 goal_slots = [],
37 goal_cb = None,
38 goal_cb_args = [],
39 goal_cb_kwargs = {},
40
41 result_key = None,
42 result_slots = [],
43 result_cb = None,
44 result_cb_args = [],
45 result_cb_kwargs = {},
46
47 input_keys = [],
48 output_keys = [],
49 outcomes = [],
50
51 exec_timeout = None,
52 preempt_timeout = rospy.Duration(60.0),
53 server_wait_timeout = rospy.Duration(60.0)
54 ):
55 """Constructor for SimpleActionState action client wrapper.
56
57 @type action_name: string
58 @param action_name: The name of the action as it will be broadcast over ros.
59
60 @type action_spec: actionlib action msg
61 @param action_spec: The type of action to which this client will connect.
62
63 @type goal: actionlib goal msg
64 @param goal: If the goal for this action does not need to be generated at
65 runtime, it can be passed to this state on construction.
66
67 @type goal_key: string
68 @param goal_key: Pull the goal message from a key in the userdata.
69 This will be done before calling the goal_cb if goal_cb is defined.
70
71 @type goal_slots: list of string
72 @param goal_slots: Pull the goal fields (__slots__) from like-named
73 keys in userdata. This will be done before calling the goal_cb if
74 goal_cb is defined.
75
76 @type goal_cb: callable
77 @param goal_cb: If the goal for this action needs to be generated at
78 runtime, a callback can be stored which modifies the default goal
79 object. The callback is passed two parameters:
80 - userdata
81 - current stored goal
82 The callback returns a goal message.
83
84 @type result_key: string
85 @param result_key: Put the result message into the userdata with
86 the given key. This will be done after calling the result_cb
87 if result_cb is defined.
88
89 @type result_slots: list of strings
90 @param result_slots: Put the result message fields (__slots__)
91 into the userdata with keys like the field names. This will be done
92 after calling the result_cb if result_cb is defined.
93
94 @type result_cb: callable
95 @param result_cb: If result information from this action needs to be
96 stored or manipulated on reception of a result from this action, a
97 callback can be stored which is passed this information. The callback
98 is passed three parameters:
99 - userdata (L{UserData<smach.user_data.UserData>})
100 - result status (C{actionlib.GoalStatus})
101 - result (actionlib result msg)
102
103 @type exec_timeout: C{rospy.Duration}
104 @param exec_timeout: This is the timeout used for sending a preempt message
105 to the delegate action. This is C{None} by default, which implies no
106 timeout.
107
108 @type preempt_timeout: C{rospy.Duration}
109 @param preempt_timeout: This is the timeout used for aborting after a
110 preempt has been sent to the action and no result has been received. This
111 timeout begins counting after a preempt message has been sent.
112
113 @type server_wait_timeout: C{rospy.Duration}
114 @param server_wait_timeout: This is the timeout used for aborting while
115 waiting for an action server to become active.
116 """
117
118
119 State.__init__(self, outcomes=['succeeded','aborted','preempted'])
120
121
122 self._action_name = action_name
123 self._action_spec = action_spec
124
125
126 self._goal_status = 0
127 self._goal_result = None
128 self._exec_timeout = exec_timeout
129 self._preempt_timeout = preempt_timeout
130 self._server_wait_timeout = server_wait_timeout
131
132
133 if goal and hasattr(goal, '__call__'):
134 raise smach.InvalidStateError("Goal object given to SimpleActionState that IS a function object")
135 sl = action_spec().action_goal.goal.__slots__
136 if not all([s in sl for s in goal_slots]):
137 raise smach.InvalidStateError("Goal slots specified are not valid slots. Available slots: %s; specified slots: %s" % (sl, goal_slots))
138 if goal_cb and not hasattr(goal_cb, '__call__'):
139 raise smach.InvalidStateError("Goal callback object given to SimpleActionState that IS NOT a function object")
140
141
142 if goal is None:
143 self._goal = copy.copy(action_spec().action_goal.goal)
144 else:
145 self._goal = goal
146
147
148 self._goal_key = goal_key
149 if goal_key is not None:
150 self.register_input_keys([goal_key])
151
152
153 self._goal_slots = goal_slots
154 self.register_input_keys(goal_slots)
155
156
157 self._goal_cb = goal_cb
158 self._goal_cb_args = goal_cb_args
159 self._goal_cb_kwargs = goal_cb_kwargs
160 if smach.has_smach_interface(goal_cb):
161 self._goal_cb_input_keys = goal_cb.get_registered_input_keys()
162 self._goal_cb_output_keys = goal_cb.get_registered_output_keys()
163
164 self.register_input_keys(self._goal_cb_input_keys)
165 self.register_output_keys(self._goal_cb_output_keys)
166 else:
167 self._goal_cb_input_keys = input_keys
168 self._goal_cb_output_keys = output_keys
169
170
171 if result_cb and not hasattr(result_cb, '__call__'):
172 raise smach.InvalidStateError("Result callback object given to SimpleActionState that IS NOT a function object")
173 if not all([s in action_spec().action_result.result.__slots__ for s in result_slots]):
174 raise smach.InvalidStateError("Result slots specified are not valid slots.")
175
176
177 self._result_cb = result_cb
178 self._result_cb_args = result_cb_args
179 self._result_cb_kwargs = result_cb_kwargs
180
181 if smach.has_smach_interface(result_cb):
182 self._result_cb_input_keys = result_cb.get_registered_input_keys()
183 self._result_cb_output_keys = result_cb.get_registered_output_keys()
184 self._result_cb_outcomes = result_cb.get_registered_outcomes()
185
186 self.register_input_keys(self._result_cb_input_keys)
187 self.register_output_keys(self._result_cb_output_keys)
188 self.register_outcomes(self._result_cb_outcomes)
189 else:
190 self._result_cb_input_keys = input_keys
191 self._result_cb_output_keys = output_keys
192 self._result_cb_outcomes = outcomes
193
194
195 self._result_key = result_key
196 if result_key is not None:
197 self.register_output_keys([result_key])
198
199
200 self._result_slots = result_slots
201 self.register_output_keys(result_slots)
202
203
204 self.register_input_keys(input_keys)
205 self.register_output_keys(output_keys)
206 self.register_outcomes(outcomes)
207
208
209 self._activate_time = rospy.Time.now()
210 self._preempt_time = rospy.Time.now()
211 self._duration = rospy.Duration(0.0)
212 self._status = SimpleActionState.WAITING_FOR_SERVER
213
214
215 self._action_client = SimpleActionClient(action_name, action_spec)
216 self._action_wait_thread = threading.Thread(name=self._action_name+'/wait_for_server', target=self._wait_for_server)
217 self._action_wait_thread.start()
218
219 self._execution_timer_thread = None
220
221
222 self._done_cond = threading.Condition()
223
225 """Internal method for waiting for the action server
226 This is run in a separate thread and allows construction of this state
227 to not block the construction of other states.
228 """
229 timeout_time = rospy.get_rostime() + self._server_wait_timeout
230 while self._status == SimpleActionState.WAITING_FOR_SERVER and not rospy.is_shutdown() and not rospy.get_rostime() >= timeout_time:
231 try:
232 if self._action_client.wait_for_server(rospy.Duration(1.0)):
233 self._status = SimpleActionState.INACTIVE
234 if self.preempt_requested():
235 return
236 except:
237 if not rospy.core._in_shutdown:
238 rospy.logerr("Failed to wait for action server '%s'" % (self._action_name))
239
241 """Internal method for cancelling a timed out goal after a timeout."""
242 while self._status == SimpleActionState.ACTIVE and not rospy.is_shutdown():
243 try:
244 rospy.sleep(0.1)
245 except:
246 if not rospy.is_shutdown():
247 rospy.logerr("Failed to sleep while running '%s'" % self._action_name)
248 if rospy.Time.now() - self._activate_time > self._exec_timeout:
249 rospy.logwarn("Action %s timed out after %d seconds." % (self._action_name, self._exec_timeout.to_sec()))
250
251 self._action_client.cancel_goal()
252
253
255 rospy.loginfo("Preempt requested on action '%s'" % (self._action_name))
256 smach.State.request_preempt(self)
257 if self._status == SimpleActionState.ACTIVE:
258 rospy.loginfo("Preempt on action '%s' cancelling goal: \n%s" % (self._action_name, str(self._goal)))
259
260 self._action_client.cancel_goal()
261
263 """Called when executing a state.
264 This calls the goal_cb if it is defined, and then dispatches the
265 goal with a non-blocking call to the action client.
266 """
267
268
269 if self._status is SimpleActionState.WAITING_FOR_SERVER:
270 rospy.logwarn("Still waiting for action server '%s' to start... is it running?" % self._action_name)
271 if self._action_wait_thread.is_alive():
272
273 self._action_wait_thread.join()
274 else:
275
276 self._wait_for_server()
277
278 if not self.preempt_requested():
279
280 rospy.loginfo("Connected to action server '%s'." % self._action_name)
281
282
283 if self.preempt_requested():
284 rospy.loginfo("Preempting %s before sending goal." % self._action_name)
285 self.service_preempt()
286 return 'preempted'
287
288
289 if self._status is SimpleActionState.WAITING_FOR_SERVER:
290 rospy.logfatal("Action server for "+self._action_name+" is not running.")
291 return 'aborted'
292 else:
293 self._status = SimpleActionState.INACTIVE
294
295
296 if self._goal_key is not None:
297 self._goal = ud[self._goal_key]
298
299
300 for key in self._goal_slots:
301 setattr(self._goal, key, ud[key])
302
303
304 if self._goal_cb is not None:
305 try:
306 goal_update = self._goal_cb(
307 smach.Remapper(
308 ud,
309 self._goal_cb_input_keys,
310 self._goal_cb_output_keys,
311 []),
312 self._goal,
313 *self._goal_cb_args,
314 **self._goal_cb_kwargs)
315 if goal_update is not None:
316 self._goal = goal_update
317 except:
318 rospy.logerr("Could not execute goal callback: "+traceback.format_exc())
319 return 'aborted'
320
321
322 if self._goal is None and self._goal_cb is None:
323 rospy.logerr("Attempting to activate action "+self._action_name+" with no goal or goal callback set. Did you construct the SimpleActionState properly?")
324 return 'aborted'
325
326
327 self._activate_time = rospy.Time.now()
328 self._status = SimpleActionState.ACTIVE
329
330
331 self._done_cond.acquire()
332 self._action_client.send_goal(self._goal, self._goal_done_cb, self._goal_active_cb, self._goal_feedback_cb)
333
334
335 if self._exec_timeout:
336 self._execution_timer_thread = threading.Thread(name=self._action_name+'/preempt_watchdog', target=self._execution_timer)
337 self._execution_timer_thread.start()
338
339
340 self._done_cond.wait()
341
342
343 result_cb_outcome = None
344 if self._result_cb is not None:
345 try:
346 result_cb_outcome = self._result_cb(
347 smach.Remapper(
348 ud,
349 self._result_cb_input_keys,
350 self._result_cb_output_keys,
351 []),
352 self._goal_status,
353 self._goal_result)
354 if result_cb_outcome is not None and result_cb_outcome not in self.get_registered_outcomes():
355 rospy.logerr("Result callback for action "+self._action_name+", "+str(self._result_cb)+" was not registered with the result_cb_outcomes argument. The result callback returned '"+str(result_cb_outcome)+"' but the only registered outcomes are: "+str(self.get_registered_outcomes()))
356 return 'aborted'
357 except:
358 rospy.logerr("Could not execute result callback: "+traceback.format_exc())
359 return 'aborted'
360
361 if self._result_key is not None:
362 ud[self._result_key] = self._goal_result
363
364 for key in self._result_slots:
365 ud[key] = getattr(self._goal_result, key)
366
367
368 if self._status == SimpleActionState.INACTIVE:
369
370 if self._goal_status == GoalStatus.SUCCEEDED:
371 outcome = 'succeeded'
372 elif self._goal_status == GoalStatus.PREEMPTED and self.preempt_requested():
373 outcome = 'preempted'
374 self.service_preempt()
375 else:
376
377
378
379 outcome = 'aborted'
380 else:
381
382 rospy.logwarn("Action state terminated without going inactive first.")
383 outcome = 'aborted'
384
385
386 if result_cb_outcome is not None:
387 outcome = result_cb_outcome
388
389
390 self._status = SimpleActionState.INACTIVE
391 self._done_cond.release()
392
393 return outcome
394
395
397 """Goal Active Callback
398 This callback starts the timer that watches for the timeout specified for this action.
399 """
400 rospy.logdebug("Action "+self._action_name+" has gone active.")
401
403 """Goal Feedback Callback"""
404 rospy.logdebug("Action "+self._action_name+" sent feedback.")
405
407 """Goal Done Callback
408 This callback resets the active flags and reports the duration of the action.
409 Also, if the user has defined a result_cb, it is called here before the
410 method returns.
411 """
412 def get_result_str(i):
413 strs = ('PENDING','ACTIVE','PREEMPTED','SUCCEEDED','ABORTED','REJECTED','LOST')
414 if i < len(strs):
415 return strs[i]
416 else:
417 return 'UNKNOWN ('+str(i)+')'
418
419
420 self._duration = rospy.Time.now() - self._activate_time
421 rospy.logdebug("Action "+self._action_name+" terminated after "\
422 +str(self._duration.to_sec())+" seconds with result "\
423 +get_result_str(self._action_client.get_state())+".")
424
425
426 self._goal_status = result_state
427 self._goal_result = result
428
429
430 self._status = SimpleActionState.INACTIVE
431
432
433 self._done_cond.acquire()
434 self._done_cond.notify()
435 self._done_cond.release()
436