API review

Proposer: Jonathan Bohren

Present at review:

  • List reviewers

Question / concerns / comments

SMACH Construction API

The goals of the SMACH construction API are:

  1. Make constructing SMACH trees as easy as possible
  2. Suppress the likelihood errors in tree specification
  3. Maintain readability of the construction calls
  4. Maintain the flexibility with which states can be moved around

Current "pre-0.2.0" API

The "pre-0.2.0" API is the API that the pr2_plugs branch and pr2_doors use. It has several features which distinguish it from the 0.1.0 API. These include:

  • State machines can be nested
  • State names and transitions are only stored in the container
  • States interface with containers via "outcomes" which are mapped to other states in a container
  • State outcomes can be mapped to outcomes of a container, instead of using proxy states for container outcomes
  • State labels, objects, and transitions are added as lists of tuples

This first-pass API attempted to make it easy to nest states on construction, so that the code clearly showed the structure of the SMACH tree at a glance. This was accomplished by making the add() methods of the container return themselves.

While good-intentioned, this resulted in a mess of parentheses and braces that would only serve to confuse someone into thinking they were writing LISP code. All containers still needed to be constructed out-of-line (like at the beginning of the function scope). The type of information being added and stored in various places is good, but this does not accomplish the API syntax goals.

The following is a contrived hierarchical state machine written with the "pre-0.2.0" API.

   1   # Construct containers
   2   sm = StateMachine(['aborted','preempted'])
   3   sm2 = StateMachine(['done'])
   4   sm3 = StateMachine(['done'])
   5   sm4 = StateMachine(['done'])
   6   sm5 = StateMachine(['done'])
   8   # Fill split container
   9   con_split = ConcurrentSplit(default_outcome = 'succeeded')
  10   con_split.add(
  11       ('SETTER', Setter()),
  12       ('RADICAL',
  13         sm5.add(
  14           ('T6',SPAState(),
  15             { 'succeeded':'SETTER',
  16               'aborted':'T6',
  17               'preempted':'SETTER'}),
  18           ('SETTER',Setter(),
  19             { 'done':'done'}) ) ),
  20       ('GETTER', Getter()) )
  21   con_split.add_outcome_map(({'SETTER':'done'},'succeeded'))
  23   # Fill root tree
  24   sm.add(
  25       state_machine.sequence('done',
  26         ('GETTER1', Getter(), {}),
  27         ('S2',
  28           sm2.add(
  29             ('SETTER', Setter(), {'done':'A_SPLIT'}),
  30             ('A_SPLIT', con_split, {'succeeded':'done'}) ), {} ),
  31         ('S3',
  32           sm3.add(
  33             ('SETTER', Setter(), {'done':'RADICAL'}),
  34             ('RADICAL',
  35               sm4.add(
  36                 ('T5',SPAState(),
  37                   { 'succeeded':'SETTER',
  38                     'aborted':'T5',
  39                     'preempted':'SETTER'}),
  40                   ('SETTER',Setter(),{'done':'done'}) ),
  41               {'done':'SETTER2'} ),
  42             ('SETTER2', Setter(), {'done':'done'}) ),
  43           {'done':'TRINARY!'} ) ) )
  45   sm.add(('TRINARY!', SPAState(),
  46       {'succeeded':'T2','aborted':'T3','preempted':'T4'}))
  47   sm.add(
  48       state_machine.sequence('succeeded',
  49         ('T2',SPAState(),{}),
  50         ('T3',SPAState(),{'aborted':'S2'}),
  51         ('T4',SPAState(),{'succeeded':'GETTER2','aborted':'TRINARY!'}) ) )
  53   sm.add(('GETTER2', Getter(), {'done':'GETTER1'}))
  55   # Set default initial states
  56   sm.set_initial_state(['GETTER1'],smach.UserData())
  57   sm2.set_initial_state(['SETTER'],smach.UserData())
  58   sm3.set_initial_state(['SETTER'],smach.UserData())
  59   sm4.set_initial_state(['T5'],smach.UserData())
  60   sm5.set_initial_state(['T6'],smach.UserData())

Some of the most confusing parts of the "pre-0.2.0" API are also the most useful. Convenient macros that do things like automatically connect up a bunch of states, or apply a set of transitions to a group of states, for example. These macros are just static functions that take in some args and a list of state specifications and output a corresponding list of modified state specifications.

Any structure that may be apparent if the state machine construction code is written like this is lost if the writer does not obey strict indentation style. Not only that, but they are not required to use it at all; the API allows SMACH trees to be constructed in many different, unreadable ways.

Proposed SMACH 0.2.0 API

A new API (along with several predacessors) has been designed to address these syntactical issues. The proposed API uses Python's context managers to keep track of the mode in which states are added to containers. This provides the readability and clearness while adding well-defined context management for catching errors in specification.

The main idea is to treat the containers themselves, or methods called on the containers as context managers. They enable the user to add states to the containers only within that block. With this API you can do things like:

Simply add states to a single state machine

   1 sm0 = StateMachine(['done','failed','preempted'])
   2 sm0.set_initial_state(['FOO'])
   4 with sm0:
   5   # Add some states
   6   sm0.add('FOO',SPAState(...),{'succeeded':'BAR','aborted':'failed'})
   7   sm0.add('BAR',SPAState(...),{'succeeded':'done','aborted':'FOO'})

Nest two state machines

   1 sm0 = StateMachine(['done','failed','preempted'])
   2 sm1 = StateMachine(['done','failed','preempted'])
   4 sm0.set_initial_state(['FOO'])
   5 sm1.set_initial_state(['DEEP'])
   7 with sm0:
   8   # Add some states
   9   sm0.add('FOO',SPAState(...),{'succeeded':'BAR','aborted':'failed'})
  10   sm0.add('BAR',SPAState(...),{'succeeded':'BAZ','aborted':'FOO'})
  11   sm0.add('BAZ',sm1,{'succeeded':'done','aborted':'BAR'})
  13   # Open the nested state machine
  14   with sm1:
  15     sm1.add('DEEP',SPAState(...),{'aborted':'DEEP'})

Nest two state machines, constructing the nested one inline

   1 sm0 = StateMachine(['done','failed','preempted'])
   3 with sm0:
   4   sm0.set_initial_state(['FOO'])
   6   sm0.add('FOO',SPAState(...),{'succeeded':'BAR','aborted':'failed'})
   7   sm0.add('BAR',SPAState(...),{'succeeded':'BAZ','aborted':'FOO'})
   9   # Create nested state machine
  10   sm1 = StateMachine(['done','failed','preempted'])
  12   # Add and open the nested state machine
  13   with sm0.add('BAZ',sm1,{'succeeded':'done','aborted':'BAR'}) as local_sm:
  14     local_sm.set_initial_state(['DEEP'])
  16     local_sm.add('DEEP',SPAState(...),{'aborted':'DEEP'})

Add states to a state machine such that each state's 'succeeded' label transitions to the next state

   1 sm0 = StateMachine(['done','failed','preempted'])
   2 sm0.set_initial_state(['FOO'])
   4 with sm0.add_connected_by_outcome('succeeded'):
   5   # Add some states
   6   sm0.add('FOO',SPAState(...),{'aborted':'failed'})
   7   sm0.add('BAR',SPAState(...),{'aborted':'FOO'})
   8   sm0.add('BAZ',SPAState(...),{'succeeded':'done','aborted':'FOO'})

Contrived SMACH example

   1 sm0 = StateMachine(['aborted','preempted'])
   2 sm0.set_initial_state(['GETTER1'])
   4 sm1 = StateMachine(['done'])
   5 sm1.set_initial_state(['SETTER'])
   7 sm2 = StateMachine(['done'])
   8 sm2.set_initial_state(['SETTER'])
  10 sm3 = StateMachine(['done'])
  11 sm3.set_initial_state(['T5'])
  13 sm4 = StateMachine(['done'])
  14 sm4.set_initial_state(['T6'])
  16 cs1 = ConcurrentSplit(['succeeded'])
  18 with sm0.add_connected_by_outcome('done'):
  19   sm0.add('GETTER1', Getter())
  21   with sm0.add('S2',sm1):
  22     sm1.set_initial_state(['SETTER'])
  23     sm1.add('SETTER', Setter(), {'done':'A_SPLIT'})
  25     with sm1.add('A_SPLIT',cs1):
  26       cs1.add_outcome_map(({'SETTER':'done'},'succeeded'))
  28       cs1.add('SETTER',Setter())
  29       cs1.add('GETTER',Getter())
  31       with cs1.add('RADICAL',sm4):
  32         sm4.add('T6', SPAState(),
  33           { 'succeeded':'SETTER',
  34             'aborted':'T6',
  35             'preempted':'SETTER'})
  36         sm4.add('SETTER',Setter(), { 'done':'done'})
  38   with sm0.add('S3',sm2,{'done':'TRINARY!'}):
  39     sm2.add('SETTER', Setter(), {'done':'RADICAL'})
  40     sm2.set_initial_state(['SETTER'])
  42     with sm2.add('RADICAL',sm3,{'done':'SETTER2'}):
  43       sm3.set_initial_state(['T5'])
  44       sm3.add('T5',SPAState(),
  45           { 'succeeded':'SETTER',
  46             'aborted':'T5',
  47             'preempted':'SETTER'})
  48       sm3.add('SETTER',Setter(),{'done':'done'})
  50     sm0.add('SETTER2', Setter(), {'done':'done'})
  52 with sm0.opened():
  53   sm0.add('TRINARY!',SPAState(),
  54       {'succeeded':'T2','aborted':'T3','preempted':'T4'})
  56 with sm0.connected_by_outcome('succeeded'):
  57   sm0.add('T2',SPAState(),{})
  58   sm0.add('T3',SPAState(),{'aborted':'S2'})
  59   sm0.add('T4',SPAState(),{'succeeded':'GETTER2','aborted':'TRINARY!'})

Meeting agenda

To be filled out by proposer based on comments gathered during API review period


  • The proposed API certainly looks better for those of us that view lisp with distrust.
  • The semantics of what gets defined are still a little opaque to me. For instance, what is the distinction in the contrived example between setting the initial state for sm1 initially with the same call as a nested state machine?
  • Even for a 0.2 release I think there needs to be some more documentation at the wiki level. Specifically, talk about the ROS action API and how that interacts with smach and make some tutorials in the form of use cases. It'd also be great to illustrate the use cases with smach_viewer screen shots.


  • I definitely agree with the need for tutorials at the wiki level. The introductory information is very good, but then goes into some specifics which are lost on non-experts. One or two tutorials illustrated with screenshots from the GUI would go a long way.
  • There seems to be some conceptual difference between how other States and nested State Machines are added to an existing State. In the "Nested two state machines" example, I do not understand how sm1 gets "added" to sm0. In the "Nest two state machines, constructing the nested one inline" example, it seems one needs to jump through some additional hoops, which is a bit confusing.


Package status change mark change manifest)

  • /!\ Action items that need to be taken.

  • {X} Major issues that need to be resolved

Wiki: smach/Reviews/2010-05-10_API_Review (last edited 2010-07-16 01:06:40 by JonathanBohren)