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