Exploring Tekkotsu Programming on Mobile Robots:

State Machine Nodes and Transitions

Prev: Walking
Up: Contents
Next: Shorthand notation

Contents: State machines, Types of nodes and transitions, BarkHowl behavior, Event logging, Multiple sources/destinations, BarkHowlBlink behavior

State Machines

The most common approach to implementing complex robot behaviors is to define a state machine, with each state performing some action, and transitions between states triggered by events such as action completions, sensor events, or timer expirations. Tekkotsu supports this by providing StateNode and Transition classes, with a variety of useful subclasses. State machines are fully integrated into the Tekkotsu event formalism: both StateNodes and Transitions are subclasses of BehaviorBase, which is in turn a subclass of EventListener.

Tekkotsu includes a special shorthand notation that makes it very easy to construct state macines. The shorthand is automatically translated to C++ code when you type "make". But in order to understand what the shorthand is doing, this section shows you how to manually construct a state machine by instantiating state node classes and calling their methods.

The diagram below shows a simple state machine comprising three nodes:

A state is activated by calling its start() method. This will in turn call StateNode::start(), which calls the start() methods of all the Transitions leading out of that state. Each Transition sets up one or more listeners for various types of events. If an event is received and the Transition fires, it deactivates the source state by calling its stop() method, and then activates the destination state by calling its start() method. Deactivating the source state causes the deactivation of all the source's outgoing Transitions (via their stop() methods), which causes them to remove their listeners.

Tekkotsu's state machine structure is recursive: any node can contain another entire state machine inside it. The normal way to define a robot behavior as a state machine is to create a single StateNode with a name like "BarkHowlBehavior" that will show up in the Mode Switch menu. This parent node provides a setup() method whose job is to instantiate all the nodes and transitions that make up the state machine. Then, when BarkHowlBehavior's start() method is called, it activates the state machine's start node, which in turn activates the outgoing transitions, and the machine is off and running.

It's important to understand the distinction between setup() and start(). The setup() method constructs the state machine but does not activate it. When the parent node's start() is called and the state machine begins operation, various child nodes and transitions will have their start() and stop() methods called as execution proceeds. When a node or transition is deactivated by stop(), it does not go away; it stays around and may be reactivated later. But when we are done with a state machine and want to discard it, we call the parent node's teardown() method to free up all its children. Each StateNode maintains a list of its children, and each child has a list of its transitions, so the teardown proceeds recursively until all the states and transitions have been deleted.

Types of Nodes and Transitions

Here are some of the most common state node types. Additional types will be covered in later sections of the tutorial.

LedNode, HeadPointerNode, WalkNode, and PostureNode each encapsulate a different type of motion command, the low-level mechanism that Tekkotsu uses to control the robot's effectors. Because of the abstraction provided by these state node classes, we can defer discussion of
motion command details to later.

Transitions also come in a variety of flavors. Here are the most basic types.

Exercise: Bark/Howl State Machine

Once again: this section of the tutorial shows the nuts and bolts of how state machines are built. Actual state machine programming in Tekkotsu is done in a shorthand notation that is much more convenient. The shorthand expands into code like what you'll see below.

To construct a state machine we must include the header file Behaviors/StateMachine.h. This in turn loads header files for all the state node and transition classes. The subclasses of StateNode can be found in Behaviors/Nodes, and the subclasses of Transition can be found in Behaviors/Transitions.

#include "Behaviors/StateMachine.h"

Note below that BarkHowlBehavior is a child of StateNode; this is reflected in both the class declaration and the constructor definition. StateNode is in turn a child of BehaviorBase.

class BarkHowlBehavior : public StateNode {
public:
  BarkHowlBehavior() : StateNode("BarkHowlBehavior") {}

The setup() method constructs the state machine. It will be called automatically the first time BarkHowlBehavior's start() method is called. The local variables bark_node, howl_node, and wait_node will be discarded when setup() returns, but pointers to these nodes will be retained by the StateNode itself, due to the calls to addNode(). The nodes in turn retain pointers to their outgoing transitions. StateNode provides a protected variable startnode to retain a pointer to the start node of the state machine, which in this case will be bark_node.

  virtual void setup() {
    std::cout << getName() << " is setting up the state machine." << std::endl;

    SoundNode *bark_node = new SoundNode("bark","barkmed.wav");
    SoundNode *howl_node = new SoundNode("howl","howl.wav");
    StateNode *wait_node = new StateNode("wait");
    addNode(bark_node); addNode(howl_node); addNode(wait_node);

    EventTrans *btrans = 
      new EventTrans(wait_node,
                     EventBase::buttonEGID,
                     ChiaraInfo::GreenButOffset,
                     EventBase::activateETID);
    btrans->setSound("ping.wav");
    bark_node->addTransition(btrans);

    bark_node->addTransition(new TimeOutTrans(howl_node,5000));
    howl_node->addTransition(new CompletionTrans(wait_node));
    wait_node->addTransition(new TimeOutTrans(bark_node,15000));

    startnode = bark_node;
  }

start() is called when we want to activate the state machine. It calls StateNode::start() to take care of housekeeping functions, which will in turn call BehaviorBase::start(), which will call the user's doStart() method. start() will also automatically call the start() method of the child node that we designated as the state machine's startnode in setup().

The private declarations at the bottom are necessary to avoid a compiler warning because the class contains pointer data members (the variable startnode inherited from StateNode.)

  virtual void doStart() {
    std::cout << getName() << " is starting up." << std::endl;
  }
 
  virtual void doStop() {
    std::cout << getName() << " is shutting down." << std::endl;
  }

private:  // Dummy methods to satisfy the compiler
  BarkHowlBehavior(const BarkHowlBehavior&);
  BarkHowlBehavior& operator=(const BarkHowlBehavior&);

};

REGISTER_BEHAVIOR(BarkHowlBehavior);

Compile and run this behavior to verify that it functions as described.

Event Logging

Every time a StateNode is activated or deactivated, it posts a stateMachineEGID event with source ID equal to the address of the state node, and eventTypeID equal to activateETID or deactivateETID. In addition, if the state node performs an action such as a motion command or sound play request that signals a completion, the state node will post an event of type statusETID. And every time a Transition fires, it posts a stateTransitionEGID status event. You can observe these events using Tekkotsu's event logger.

Using the Event Logger

  1. Compile the example state machine program above, and run it on the robot.

  2. In the ControllerGUI, go to Root Control > Status Reports > Event Logger, and double click on stateMachineEGID and stateTransitionEGID. You should see check marks appear next to each of them.

  3. Go back to Root Control > Mode Switch, and activate BarkHowlBehavior. You should then see a series of event notifications in the console window. The first event you should see is for the activation of BarkHowlBehavior, which is itself a StateNode. Then you will see the activation of the bark node, followed by one of two transitions, depending on whether you press a button or allow the timer to expire.

  4. Go back to the event logger and add logging for buttonEGID and audioEGID. Now when you run the state machine you will see the button press and audio events that trigger two of the state transitions.

The ordering of events in the event log may be a bit counterintuitive, e.g., in response to the button press, you will first see the EventTrans fire event, then the deactivation of the "bark" node, then the activation of the "wait" node, and finally the button press event that triggered the transition. This ordering is an artifact of the event logging algorithm, and should not affect the code you write.

To see the actual timestamp of each event, go to the event logger and scroll down to the bottom of the menu. Click on the Verbosity item, which should read "Verbosity (0)". Type a 1 in the Send Input dialog box on the right, and hit return. The menu should now read "Verbosity (1)", and each line of the event log will contain two additional numbers: a duration (0 for most event types) and a timestamp.

Multiple sources and destinations

Sometimes it can be useful for a transition to have more than one source or destination. The meaning of this depends on the type of the transition, but the default convention is that when a transition fires, all of its source nodes are deactivated, and then all of its destination nodes are activated. Thus, a node that transitions to itself will be deactivated and then activated again. (This is useful if we want the node's internal variables to be reinitialized.)

One way to use multiple sources and destinations is to compose nodes together if none of the built-in node types does exactly what you want. For example, suppose we want the robot to blink its LEDs while playing the howl sound. We don't know how long the sound will last, so it should keep blinking until the sound has ended. We can achieve this by simultaneously activating a SoundNode (called Howl) and a LedNode (called Blink), as shown in the diagram below. We tell Blink to use its motion command's cycle feature to cycle the LEDs on and off.

When Howl completes, we want to deactivate Blink as well. We can do this by using an optional second argument in the CompletionTrans constructor that tells it how many source nodes must complete before the transition should fire. Although this transition has two source nodes, we will tell it to fire when it sees one of them complete. That one will always be Howl, because the cycle() instruction given to Blink's motion command will never terminate on its own.

There is one more trick to note in the example code below. The cycle() command alters the brightness of the specified LEDs in a sinusoidal pattern. When the Blink node is deactivated (because the transition has fired), it removes its motion command, which leaves the LEDs at whatever brightness level they had at the moment. Thus the LEDs could continue to glow after the sound has stopped. To prevent this, we set up a low-priority motion command in a node called NoBlink that clears the LEDs. When Blink is active it will have normal priority and will override the background motion command. When Blink deactivates and the robot moves into the Wait state, the background motion command will return the LEDs to the off state.

We want to launch this background motion command at the start of the behavior. Since a state machine can only have a single start node, we create a new start node called Launch and use a null transition to immediately deactivate it and activate both Bark and NoBlink.

Here is the code for BarkHowlBlinkBehavior. Don't worry about the calls to getMC() and setPriority(); we'll cover those motion command details in a later chapter.

#include "Behaviors/StateMachine.h"

class BarkHowlBlinkBehavior : public StateNode {
public:
  BarkHowlBlinkBehavior() : StateNode("BarkHowlBlinkBehavior") {}
 
  virtual void setup() {
    std::cout << getName() << " is setting up the state machine." << std::endl;

    StateNode *launch   = new StateNode("launch");
    LedNode   *noblink    = new LedNode("noblink");
    SoundNode *bark_node  = new SoundNode("bark","barkmed.wav");
    SoundNode *howl_node  = new SoundNode("howl","howl.wav");
    LedNode   *blink_node = new LedNode("blink");
    StateNode *wait_node  = new StateNode("wait");

    addNode(launch); addNode(noblink);
    addNode(bark_node); addNode(howl_node); addNode(blink_node); addNode(wait_node);

    NullTrans *ntrans = new NullTrans(bark_node);
    ntrans->addDestination(noblink);
    launch->addTransition(ntrans);

    noblink->getMC()->set(RobotInfo::AllLEDMask, 0.0);
    noblink->setPriority(MotionManager::kBackgroundPriority);

    EventTrans *btrans = new EventTrans(wait_node,
					EventBase::buttonEGID,
					ChiaraInfo::GreenButOffset,
					EventBase::activateETID);
    btrans->setSound("ping.wav");
    bark_node->addTransition(btrans);

    blink_node->getMC()->cycle(RobotInfo::AllLEDMask, 1500, 1.0);

    TimeOutTrans *htrans = new TimeOutTrans(howl_node,5000);
    htrans->addDestination(blink_node);
    bark_node->addTransition(htrans);

    CompletionTrans *ctrans = new CompletionTrans(wait_node,1);
    howl_node->addTransition(ctrans);
    blink_node->addTransition(ctrans);

    wait_node->addTransition(new TimeOutTrans(bark_node,15000));

    startnode = launch;
  }

  virtual void doStart() {
    std::cout << getName() << " is starting up." << std::endl;
  }
 
  virtual void doStop() {
    std::cout << getName() << " is shutting down." << std::endl;
  }

protected:  // Dummy methods to satisfy the compiler
  BarkHowlBlinkBehavior(const BarkHowlBlinkBehavior&);
  BarkHowlBlinkBehavior& operator=(const BarkHowlBlinkBehavior&);

};

REGISTER_BEHAVIOR(BarkHowlBlinkBehavior);

Compile and run this behavior to verify that it functions as described.

Explore more:

  1. Suppose that instead of specifying two targets for the NullTrans in the above example, we had two separate NullTrans transitions leading out of the Launch state. Why wouldn't this work? Hint: what happens when the first NullTrans fires? You may want to look at the code for Transition.h.
  2. There is no transition leading out of the NoBlink state. When does this behavior terminate?
Prev: Walking
Up: Contents
Next: Shorthand notation


Last modified: Wed Jan 19 00:58:07 EST 2011