Exploring Tekkotsu Programming on Mobile Robots:

Defining New Node Classes

Prev: Shorthand notation
Up: Contents
Next: The Storyboard tool

Contents: Defining a simple node class, Defining methods, Initializers, Constructor arguments, Sample program, Advanced concepts

Defining a simple node class

You can define new node classes by writing a definition in C++ and specifying StateNode or one of its children as a parent class of your class. However, this can be tedious because you must write out the parent information, define a constructor, and then define any methods you want to override. Furthermore, your class must obey certain conventions common to all state nodes, such as taking a node name as an optional first argument. To ease this burden, the stateparser offers a shorthand notation for quickly defining new classes of state nodes. We'll introduce this notation in a series of simple steps.

You can define a new node class using the $nodeclass shorthand notation by specifying the name of the class and at least one parent class, like so:

$nodeclass MyUselessClass : StateNode {
}

The above definition expands into the following C++ code:

class MyUselessClass : public StateNode {
public:
  MyUselessClass(const std::string &nodename = "MyUselessClass") : StateNode(nodename) {}
};

One way to define a not-so-useless class is to pass some extra arguments to the parent's constructor. In the shorthand notation, instead of writing the name of a parent class you can write a call to its constructor, and the stateparser will do the rest. In any parent constructor call, the special symbol $ will be substituted for the nodename argument. Example:

$nodeclass SayHello : SpeechNode($,"Hello there!") {
}

Now we can use SayHello in a state machine definition, just like any of the built-in state node classes.

Defining node methods

Another way to make a useful new class is to put some code in the new class's doStart method. Example:

$shortnodeclass WriteNodeName : StateNode {
  virtual void doStart() {
    cout << "This is node " << getName() << endl;
  }
}

Here's another trick: to turn on a robot's green LED using a LedNode, you must tell the LedMC motion command inside the LedNode which LED you want to operate on, and what you want to do with it. To simplify things, you can make a new class called GreenLEDOn and include code in its constructor to supply the necessary information to the motion command. To do that, you specify an optional method argument on the $nodeclass line; in this case it's the constructor method that you want to add to:

$nodeclass GreenLEDOn : LEDNode : constructor {
  getMC()->set(RobotInfo::GreenLEDMask, 1.0);
}

Here's an example of a class with both doStart and doEvent methods. This class waits for the user to press the green button on the robot, and then posts a completion event. (Normally we would use an EventTrans to look for a button event, and we might abbreviate it using a =B(...)=> shorthand transition. Here we're handling the button event in an unusual way just for illustrative purposes.)

$nodeclass WaitForPress : StateNode {
  virtual void doStart() {
    erouter->addListener(this, EventBase::buttonEGID, RobotInfo::GreenButOffset, EventBase::activateETID);
  }

  virtual void doEvent() {
    cout << "Green button pressed: "  << event->getDescription() << endl;
    postStateCompletion();
  }

}

Initializers

If you don't specify a method type (such as "constructor") in the $nodeclass declaration, then the lines that follow will be part of the class definition rather than a method definition. This is useful if you want to define member variables for the class. But then you'll need to intiialize them. You can do that by writing the initializers as the third argument of the $nodeclass declaration:

$nodeclass WaitFor3Presses : StateNode : count() {
  int count;

  virtual void doStart() {
    count = 0;
    erouter->addListener(this, EventBase::buttonEGID, 
                        RobotInfo::GreenButOffset, EventBase::activateETID);
  }

  virtual void doEvent() {
    if ( ++count == 3 )
      postStateCompletion();
  }

}

You might be wondering why count is explicitly initialized to 0 in the doStart method instead of relying on the constructor's initializer list. The reason is that the constructor is only called to instantiate the class and produce an instance. If that state node instance is part of a loop it will be entered multiple times, and the counter needs to be reset each time. On the other hand, if the variable count did not have an intiailizer expression in the $nodeclass declaration, we would get a compiler warning. So an initializer must be supplied, but in this example the initial value it assigns is not being relied upon.

Constructor arguments

When you define a new node class, the default constructor will accept only an optional node name argument. However, you may need the constructor to accept additional arguments. You can specify the argument list as part of the $nodeclass declaration, and the stateparser will automatically generate variable declarations and initializers to cache those arguments so you can refer to them in any methods you care to write. But if you need to refer to the arguments in a call to a parent class constructor, you must precede the argument name with an underscore.

Here's an example: suppose we want to define a node class GoForward that takes a distance argument in its constructor, makes the robot travel forward by that distance, and also announces the distance in a message on the console. If we're coding for the the Create we would use a WalkNode to do the moving. Here is the class definition:

$nodeclass GoForward(float distance) : WalkNode($, distance, 0, 0, 1) {
  virtual void doStart() {
    cout << "Going forward by " << distance << " mm." << endl;
  }
}

There are several things to note in the above example. First, even though we explicitly specified the form of the GoForward constructor, we did not have to include the nodename argument; that is supplied automatically by the stateparser and cannot be overridden. Second, we did not have to write an initializer expression for distance; this was also handled automatically by the state parser. The declaration above expands into the following C++ code:

class GoForward : public WalkNode {
 public:
  float distance;  // cache the constructor's parameter
  GoForward(const std::string &nodename, float _distance) : WalkNode(nodename,_distance,0,0,1), distance(_distance) {}
  virtual void doStart() {
    cout << "Going forward by " << distance << " mm." << endl;
  }
};

Note that since we did not specify a default value for the distance argument, a value must be supplied in the constructor call, and this in turn means a value must be supplied for the nodename argument that precedes it. Thus, with the above definition for GoForward, this expression is not legal:

  fwd: GoForward =C=> SayHello
Instead we must write something like:
  fwd: GoForward($,100) =C=> SayHello
But we could, if we wish, specify a default value for the distance in the usual way:

$nodeclass GoForward(float distance=100) : WalkNode($,_distance,0,0,1) {
  virtual void doStart() {
    cout << "Going forward by " << distance << " mm." << endl;
  }
}

A Sample Program

To show you how the pieces fit together, here is a complete sample behavior defined in shorthand notation. Notice that the definitions of the SayHello and PlayGoodBye classes are nested inside the MySampleBehavior class. This ensures that they do not interfere with other behaviors which may have other definitions for these names.

#include "Behaviors/StateMachine.h"

$nodeclass MySampleBehavior : StateNode {
  $nodeclass SayHello : SpeechNode($,"Hello") : doStart {
    cout << "Hello and welcome to my robot lab." << endl;
  }

  $nodeclass SayGoodBye : SoundNode($,"crash.wav") : doStart {
    cout << "Thanks for coming.  See you soon!" << endl;
  }

  $setupmachine{
    startnode: SayHello =T(5000)=> SayGoodBye
  }

}
REGISTER_BEHAVIOR(MySampleBehavior);

Advanced Concepts

Prev: Shorthand notation
Up: Contents
Next: The Storyboard tool


Last modified: Wed Jan 19 00:47:43 EST 2011