1
2 import threading
3 import traceback
4 from contextlib import contextmanager
5
6 import asmach as smach
7 from asmach import async
8
9 __all__ = ['StateMachine']
13 """StateMachine
14
15 This is a finite state machine smach container. Note that though this is
16 a state machine, it also implements the L{smach.State}
17 interface, so these can be composed hierarchically, if such a pattern is
18 desired.
19
20 States are added to the state machine as 3-tuple specifications:
21 - label
22 - state instance
23 - transitions
24
25 The label is a string, the state instance is any class that implements the
26 L{smach.State} interface, and transitions is a dictionary mapping strings onto
27 strings which represent the transitions out of this new state. Transitions
28 can take one of three forms:
29 - OUTCOME -> STATE_LABEL
30 - OUTCOME -> None (or unspecified)
31 - OUTCOME -> SM_OUTCOME
32 """
33 - def __init__(self, outcomes, input_keys=[], output_keys=[]):
34 """Constructor for smach StateMachine Container.
35
36 @type outcomes: list of strings
37 @param outcomes: The potential outcomes of this state machine.
38 """
39
40
41 smach.container.Container.__init__(self, outcomes, input_keys, output_keys)
42
43
44 self._state_transitioning_lock = threading.Lock()
45
46
47 self._is_running = False
48
49 self._initial_state_label = None
50
51 self._current_label = None
52 self._current_state = None
53 self._current_transitions = None
54 self._current_outcome = None
55
56
57 self._preempted_label = None
58 self._preempted_state = None
59
60
61
62
63 self._states = {}
64 self._transitions = {}
65 self._remappings = {}
66
67
68 self._last_added_label = None
69 self._connector_outcomes = []
70
71
72 self._execute_thread = None
73 self.userdata = smach.UserData()
74
75
76 @staticmethod
77 - def add(label, state, transitions = None, remapping = None):
78 """Add a state to the opened state machine.
79
80 @type label: string
81 @param label: The label of the state being added.
82
83 @param state: An instance of a class implementing the L{State} interface.
84
85 @param transitions: A dictionary mapping state outcomes to other state
86 labels or container outcomes.
87
88 @param remapping: A dictrionary mapping local userdata keys to userdata
89 keys in the container.
90 """
91
92 self = StateMachine._currently_opened_container()
93
94 smach.logdebug('Adding state (%s, %s, %s)' % (label, str(state), str(transitions)))
95
96
97 if self._initial_state_label is None:
98 self._initial_state_label = label
99
100 if transitions is None:
101 transitions = {}
102
103 if remapping is None:
104 remapping = {}
105
106
107 """
108 if 'transitions' in smach.Container._context_kwargs:
109 for outcome, target in smach.Container._context_kwargs['transitions'].iteritems():
110 if outcome not in transitions:
111 transitions[outcome] = target
112 """
113
114
115 self.check_state_spec(label, state, transitions)
116
117
118 if label in self._states:
119 raise smach.InvalidStateError(
120 'Attempting to add state with label "'+label+'" to state machine, but this label is already being used.')
121
122
123 smach.logdebug("Adding state '"+str(label)+"' to the state machine.")
124
125
126 registered_outcomes = state.get_registered_outcomes()
127
128
129 missing_transitions = dict([(o,None) for o in registered_outcomes if o not in transitions.keys()])
130 transitions.update(missing_transitions)
131 smach.logdebug("State '%s' is missing transitions: %s" % (label,str(missing_transitions)))
132
133
134 self._states[label] = state
135 self._transitions[label] = transitions
136 self._remappings[label] = remapping
137 smach.logdebug("TRANSITIONS FOR %s: %s" % (label, str(self._transitions[label])))
138
139
140 if len(self._connector_outcomes) > 0 and self._last_added_label is not None:
141 for connector_outcome in self._connector_outcomes:
142 self._transitions[self._last_added_label][connector_outcome] = label
143
144 self._connector_outcomes = []
145 self._last_added_label = None
146
147 return state
148
149 @staticmethod
150 - def add_auto(label, state, connector_outcomes, transitions = None, remapping = None):
151 """Add a state to the state machine such that it automatically transitions to the next added state.
152 Each state added will receive an additional transition from it to the
153 state which is added after it. The transition will follow the outcome
154 specified at construction of this container.
155
156 @type label: string
157 @param label: The label of the state being added.
158
159 @param state: An instance of a class implementing the L{State} interface.
160
161 @param transitions: A dictionary mapping state outcomes to other state
162 labels. If one of these transitions follows the connector outcome
163 specified in the constructor, the provided transition will override
164 the automatically generated connector transition.
165 """
166
167 self = StateMachine._currently_opened_container()
168
169
170 add_ret = smach.StateMachine.add(label, state, transitions, remapping)
171
172
173 registered_outcomes = state.get_registered_outcomes()
174 if not all([co in registered_outcomes for co in connector_outcomes]):
175 raise smach.InvalidStateError("Attempting to auto-connect states with outcomes %s, but state '%s' only has registerd outcomes: %s" % (str(connector_outcomes), str(label), str(registered_outcomes)))
176
177
178 self._last_added_label = label
179 self._connector_outcomes = connector_outcomes
180
181 return add_ret
182
183
185 if state_label is not None:
186
187 self._current_label = state_label
188 self._current_state = self._states[state_label]
189 self._current_transitions = self._transitions[state_label]
190 self._current_outcome = None
191 else:
192
193 self._current_label = None
194 self._current_state = None
195 self._current_transitions = None
196 self._current_outcome = None
197
198 @async.function
200 """Method that updates the state machine once.
201 This checks if the current state is ready to transition, if so, it
202 requests the outcome of the current state, and then extracts the next state
203 label from the current state's transition dictionary, and then transitions
204 to the next state.
205 """
206 yield
207 outcome = None
208 transition_target = None
209 last_state_label = self._current_label
210
211
212 if self._current_label not in self._states.keys():
213 raise smach.InvalidStateError("State '%s' does not exist. Available states are: %s" %
214 (str(self._current_label),str(self._states.keys())))
215
216
217 if self.preempt_requested():
218 smach.loginfo("Preempt requested on state machine before executing the next state.")
219
220 if self._preempted_state is not None:
221
222 if self._preempted_state.preempt_requested():
223 smach.loginfo("Last state '%s' did not service preempt. Preempting next state '%s' before executing..." % (self._preempted_label, self._current_label))
224
225
226 self._preempt_current_state()
227 else:
228
229 self._preempt_requested = False
230 else:
231
232
233 self._preempt_current_state()
234
235
236 try:
237 self._state_transitioning_lock.release()
238 outcome = yield self._current_state.execute_async(
239 smach.Remapper(
240 self.userdata,
241 self._current_state.get_registered_input_keys(),
242 self._current_state.get_registered_output_keys(),
243 self._remappings[self._current_label]))
244 except smach.InvalidUserCodeError as ex:
245 smach.logerr("State '%s' failed to execute." % self._current_label)
246 raise ex
247 except:
248 raise smach.InvalidUserCodeError("Could not execute state '%s' of type '%s': " % ( self._current_label, self._current_state ) + traceback.format_exc())
249 finally:
250 self._state_transitioning_lock.acquire()
251
252
253 if outcome not in self._current_state.get_registered_outcomes():
254 raise smach.InvalidTransitionError("Attempted to return outcome '%s' from state '%s' of type '%s' which only has registered outcomes: %s" %
255 ( str(outcome),
256 str(self._current_label),
257 str(self._current_state),
258 str(self._current_state.get_registered_outcomes())))
259
260
261 if outcome not in self._current_transitions:
262 raise smach.InvalidTransitionError("Outcome '%s' of state '%s' is not bound to any transition target. Bound transitions include: %s" %
263 (str(outcome), str(self._current_label), str(self._current_transitions)))
264
265
266 transition_target = self._current_transitions[outcome]
267
268
269 if transition_target in self._states:
270
271 self._set_current_state(transition_target)
272
273
274 smach.loginfo("State machine transitioning '%s':'%s'-->'%s'" % (str(last_state_label), str(outcome), str(transition_target)))
275
276
277 self.call_transition_cbs()
278 else:
279
280
281 if self._preempt_requested and self._preempted_state is not None:
282 if not self._current_state.preempt_requested():
283 self.service_preempt()
284
285 if transition_target not in self.get_registered_outcomes():
286
287 transition_target = outcome
288
289 if transition_target in self.get_registered_outcomes():
290
291 self._set_current_state(None)
292
293
294 smach.loginfo("State machine terminating '%s':'%s':'%s'" % (str(last_state_label), str(outcome), str(transition_target)))
295
296
297 self.call_termination_cbs([last_state_label],transition_target)
298
299 async.returnValue(transition_target)
300 else:
301 raise smach.InvalidTransitionError("Outcome '%s' of state '%s' with transition target '%s' is neither a registered state nor a registered container outcome." %
302 (str(outcome), str(self._current_label), str(transition_target)))
303 async.returnValue(None)
304
305
306 @async.function
308 """Run the state machine on entry to this state.
309 This will set the "closed" flag and spin up the execute thread. Once
310 this flag has been set, it will prevent more states from being added to
311 the state machine.
312 """
313
314
315 with self._state_transitioning_lock:
316
317 try:
318 self.check_consistency()
319 except (smach.InvalidStateError, smach.InvalidTransitionError):
320 smach.logerr("Container consistency check failed.")
321 async.returnValue(None)
322
323
324 self._is_running = True
325
326
327 self._preempted_label = None
328 self._preempted_state = None
329
330
331 self._set_current_state(self._initial_state_label)
332
333
334 self._copy_input_keys(parent_ud, self.userdata)
335
336
337 smach.loginfo("State machine starting in initial state '%s' with userdata: \n\t%s" %
338 (self._current_label,str(self.userdata.keys())))
339
340
341
342 self.call_start_cbs()
343
344
345 container_outcome = None
346
347
348 while container_outcome is None and self._is_running and not smach.is_shutdown():
349
350 container_outcome = yield self._update_once()
351
352
353 self._copy_output_keys(self.userdata, parent_ud)
354
355
356 self._is_running = False
357
358 async.returnValue(container_outcome)
359
360
362 """Propagate preempt to currently active state.
363
364 This will attempt to preempt the currently active state.
365 """
366 with self._state_transitioning_lock:
367
368 self._preempt_requested = True
369
370 if self._current_state is not None:
371 self._preempt_current_state()
372
374 """Preempt the current state (might not be executing yet).
375 This also resets the preempt flag on a state that had previously received the preempt, but not serviced it."""
376 if self._preempted_state != self._current_state:
377 if self._preempted_state is not None:
378
379 self._preempted_state.recall_preempt()
380
381
382 self._preempted_state = self._current_state
383 self._preempted_label = self._current_label
384
385
386 try:
387 self._preempted_state.request_preempt()
388 except:
389 smach.logerr("Failed to preempt contained state '%s': %s" % (self._preempted_label, traceback.format_exc()))
390
391
394
396 if key not in self._states:
397 smach.logerr("Attempting to get state '%s' from StateMachine container. The only available states are: %s" % (key, self._states.keys()))
398 raise KeyError()
399 return self._states[key]
400
402 smach.logdebug("Setting initial state to "+str(initial_states))
403
404 if len(initial_states) > 1:
405 smach.logwarn("Attempting to set initial state to include more than one state, but the StateMachine container can only have one initial state.")
406
407
408 if len(initial_states) > 0:
409 self._initial_state_label = initial_states[0]
410
411 self.userdata.update(userdata)
412
414 return [str(self._current_label)]
415
417 return [str(self._initial_state_label)]
418
420 int_edges = []
421 for (from_label,transitions) in self._transitions.iteritems():
422 for (outcome,to_label) in transitions.iteritems():
423 int_edges.append([outcome,from_label,to_label])
424 return int_edges
425
426
428 """Validate full state specification (label, state, and transitions).
429 This checks to make sure the required variables are in the state spec,
430 as well as verifies that all outcomes referenced in the transitions
431 are registered as valid outcomes in the state object. If a state
432 specification fails validation, a L{smach.InvalidStateError} is
433 thrown.
434 """
435
436 registered_outcomes = state.get_registered_outcomes()
437 for outcome in transitions.keys():
438 if outcome not in registered_outcomes:
439 raise smach.InvalidTransitionError("Specified outcome '"+outcome+"' on state '"+label+"', which only has available registered outcomes: "+str(registered_outcomes))
440
442 """Check the entire state machine for consistency.
443 This asserts that all transition targets are states that are in the
444 state machine. If this fails, it raises an L{InvalidTransitionError}
445 with relevant information.
446 """
447
448 available_states = set(self._states.keys()+list(self.get_registered_outcomes()))
449
450
451 registered_sm_outcomes = self.get_registered_outcomes()
452
453
454 errors = ""
455
456
457 if self._initial_state_label is None:
458 errors = errors + "\n\tNo initial state set."
459 elif self._initial_state_label not in self._states.keys():
460 errors = errors + "\n\tInitial state label: '"+str(self._initial_state_label)+"' is not in the state machine."
461
462
463 state_specs = [
464 (label, self._states[label], self._transitions[label])
465 for label in self._states.keys()]
466
467 for label,state,transitions in state_specs:
468
469 transition_states = set(
470 [s for s in transitions.values() if ((s is not None) and (s != ''))] )
471
472 missing_states = transition_states.difference(available_states)
473
474
475 if len(missing_states) > 0:
476 errors = (errors
477 +"\n\tState '"+str(label)
478 +"' references unknown states: "+str(list(missing_states)))
479
480
481 terminal_outcomes = set([o for o,s in transitions.iteritems() if ((s is None) or (s == ''))])
482
483 missing_outcomes = terminal_outcomes.difference(registered_sm_outcomes)
484
485 if len(missing_outcomes) > 0:
486 errors = (errors
487 +"\n\tState '"+str(label)
488 +"' references unregistered outcomes: "+str(list(missing_outcomes)))
489
490
491 if len(errors) > 0:
492 raise smach.InvalidTransitionError("State machine failed consistency check: "+errors+"\n\n\tAvailable states: "+str(list(available_states)))
493
501
502
504 """async.returnValues true if the state machine is running."""
505 return self._is_running
506