Exploring Tekkotsu Programming on Mobile Robots:

Nodes and Transitions

Prev: Walking
Up: Contents
Next: Shorthand notation

Contents: State machines, Event logging, Multiple sources/destinations

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 sensor events. Tekkotsu supports this by providing StateNode and Transition classes. The implementation of the state machine concept is fully integrated into the Tekkotsu formalism: both StateNodes and Transitions are subclasses of BehaviorBase, which is in turn a subclass of EventListener.

A state is activated by calling its DoStart() function. This will in turn call StateNode::DoStart(), which will call the DoStart() functions of all the Transitions leading out of that state. Each Transition sets up one or more listeners for various types of events. If events of the appropriate types are received, the Transition "fires": it deactivates the source state by calling its DoStop() function, and then activates the destination state by calling its DoStart() function. Deactivating the source state causes the deactivation of all its outgoing Transitions (via their DoStop() functions), which causes them to remove their listeners.

The diagram below shows a simple state machine comprising three nodes. The state machine starts out in state Bark, which has two outgoing transitions. One sets up a listener for a button press event; the other starts a 5 second timer. When activated, the Bark state plays a bark sound and then just sits there, waiting for a transition to occur. If the user presses the head button, the AIBO will transition to state Wait, emitting a "ping" sound as it makes the transition. If a button press event does not occur within 5 seconds, the timer will expire, causing the AIBO to transition to state Howl. (Whichever transition occurs, the Bark state will be deactivated, which in turn deactivates both its outgoing transitions.) The Howl state plays a howl sound and, when the playing finishes, signals completion by posting a status event. This event is recognized by the transition leading from Howl to Wait, which set up a listener for it when Howl was activated. The Wait state does nothing, but it has an outgoing transition that sets up up a 15 second timer. When the timer expires, this transition takes the AIBO from the Wait state back to the Bark state.

Tekkotsu's state machine structure is recursive: any node can contain another entire state machine inside it. The normal way to define a behavior as a state machine is to create a single StateNode with a name like "DstBehavior". This parent node provides a setup() function whose job is to instantiate all the nodes and transitions that make up the state machine. Then, when DstBehavior's DoStart() function 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 DoStart(). The setup() function constructs the state machine but does not activate it. When the parent node's DoStart() is called and the state machine begins operation, various child nodes and transitions will have their DoStart() and DoStop() functions called. When a node or transition is deactivated, 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() function to free up al its children. Each StateNode maintains a list of its children, and each child state has a list of its transitions, so the teardown proceeds recursively until all the states and transitions have been deleted.

Exercise: Bark/Howl State Machine

To construct a state machine we must include the files Behaviors/StateNode.h and Behaviors/Transition.h. In addition, we must include the files for any their of subclasses we use. The subclasses of StateNode can be found in Behaviors/Nodes, and the subclasses of Transition can be found in Behaviors/Transitions.

#ifndef INCLUDED_DstBehavior_h_
#define INCLUDED_DstBehavior_h_
 
#include "Behaviors/StateNode.h"
#include "Behaviors/Nodes/SoundNode.h"
#include "Behaviors/Transitions/CompletionTrans.h"
#include "Behaviors/Transitions/EventTrans.h"
#include "Behaviors/Transitions/TimeOutTrans.h"
#include "Events/EventRouter.h"

Note that DstBehavior is now a child of StateNode. StateNode is a child of BehaviorBase. We use a protected variable to retain a pointer to the start node of the state machine, which in this case will be bark_node.

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

The setup() function constructs the state machine. It will be called automatically the first time DstBehavior's DoStart() function 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.

  virtual void setup() {
    StateNode::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,
                                        RobotInfo::HeadFrButOffset,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;
  }

DoStart() is called when we want to activate the state machine. It calls StateNode::DoStart() to take care of housekeeping functions; this will automatically call the DoStart() of the child node that we designated as the startnode in setup(). DoStop() likewise calls StateNode::DoStop(). 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;
    StateNode::DoStart();
  }
 
  virtual void DoStop() {
    std::cout << getName() << " is shutting down." << std::endl;
    StateNode::DoStop();
  }

private:  // Dummy functions to satisfy the compiler
  DstBehavior(const DstBehavior&);
  DstBehavior& operator=(const DstBehavior&);

};

#endif

The "dummy functions" above (a copy constructor and an assignment operator) are included to suppress a compiler warning. The warning is generated whenever a class containing pointer data members relies on default copy constructor and assignment operators.

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

Event Logging

Every time a StateNode is activated or deactivated, it posts an 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 boot the AIBO.

  2. Open a telnet connection to the robot on port 59000.

  3. 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.

  4. Go back to Root Control > Setup Mode, and activate DstBehavior. You should then see a series of event notifications in the telnet window. The first event you should see is for the activation of DstBehavior, 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.

  5. 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, and/or more than one 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, which 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 AIBO 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:

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() command given to Blink never completes.)

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 face 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 command in a node called NoBlink that clears the face LEDs. When Blink is active it will have normal priority and will override this motion command. When Blink deactivates and the robot moves into the Wait state, the background 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.

#ifndef INCLUDED_DstBehavior_h_
#define INCLUDED_DstBehavior_h_
 
#include "Behaviors/StateNode.h"
#include "Behaviors/Nodes/SoundNode.h"
#include "Behaviors/Nodes/LedNode.h"
#include "Behaviors/Transitions/CompletionTrans.h"
#include "Behaviors/Transitions/EventTrans.h"
#include "Behaviors/Transitions/NullTrans.h"
#include "Behaviors/Transitions/TimeOutTrans.h"
#include "Events/EventRouter.h"

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

    StateNode *launcher   = new StateNode("launcher");
    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(launcher); addNode(noblink);
    addNode(bark_node); addNode(howl_node); addNode(blink_node); addNode(wait_node);

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

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

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

    blink_node->getMC()->cycle(RobotInfo::FaceLEDMask,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 = launcher;
  }

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

protected:  // Dummy functions to satisfy the compiler
  DstBehavior(const DstBehavior&);
  DstBehavior& operator=(const DstBehavior&);

};

#endif

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: Mon Feb 11 06:54:20 EST 2008