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 nodeclass using the #nodeclass shorthand notation by specifying the name of the class and at least one parent class, like so:

#nodeclass MyUselessClass : StateNode
#endnodeclass

You can also use the #shortnodeclass directive, which doesn't require a matching #endnodeclass. Instead, the class definition is terminated by a blank line:

#shortnodeclass MyUselessClass : StateNode

//      (the  blank line above terminated the shortnodeclass definition)

From now on, whatever we say about #nodeclass should be understood to also apply to #shortnodeclass. The only difference is in how the class definition is terminated. Either one of the above definitions 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:

#shortnodeclass SayHello : SpeechNode($,"Hello")

Now we can use SayHello in a state machine definition, just like any of the built-in 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. A #nodeclass declaration can include an optional method name argument, which must be one of start, startEvent, DoStart, DoStartEvent, stop, DoStop, processEvent, setup, or constructor. Example:

#shortnodeclass WriteNodeName : StateNode : DoStart
  cout << "This is node " << getName() << endl;

The declaration above both defines the class WriteNodeName and specifies that the lines that follow should be taken as the body of its DoStart method.

Here's another example of the same idea. 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:

#shortnodeclass GreenLEDOn : LEDNode : constructor
  getMC()->set(RobotInfo::GreenLEDMask, 1.0);

If you need to define more than one method for a class, you can use a #nodemethod or #shortnodemethod declaration to begin the next method. Any currently open method definition will be automatically closed for you.

Here's an example of a class with both DoStart and processEvent 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.) Notice that you do not need to specify the arguments to any of the standard StateNode methods in the shorthand notation; the stateparser will automatically supply a const EventBase &event argument to those methods that accept one, or a null argument to those that don't.

#shortnodeclass WaitForPress : StateNode : DoStart
    erouter->addListener(this, EventBase::buttonEGID, RobotInfo::GreenButOffset, EventBase::activateETID);
  #shortnodemethod processEvent
    cout << "Green button pressed: "  << event.getDescription() << endl;
    postStateCompletion();

In the above example, the #shortnodemethod line terminated the DoStart definition, and the blank line at the end terminated both the processEvent definition and the class definition.

Initializers

If you don't specify a method name 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. In the example below, we use #nodeclass instead of #shortnodeclass because we want to include blank lines in the class definition, for readability:

#nodeclass WaitFor3Presses : StateNode : count()
  int count;

  #shortnodemethod DoStart
    count = 0;
    erouter->addListener(this, EventBase::buttonEGID, 
      RobotInfo::GreenButOffset, EventBase::activateETID);

  #shortnodemethod processEvent
    if ( ++count == 3 )
      postStateCompletion();

#endnodeclass

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 AIBO or the Create we would use a WalkNode to do the moving. (At present we would have to use an XWalkNode on the Chiara, but this will change soon.) Here is the class definition:

#shortnodeclass GoForward(float distance) : WalkNode($,_distance,0,0,1) : DoStart
    cout << "Going forward by " << distance << " mm." << endl;

There are severalt 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, note that the distance argument was referenced as "_distance" in the WalkNode constructor call, but as "distance" inside the DoStart method. The underscore is only necessary when variables are referenced in calls to parent constructors, or in initializer expressions; it should not be used when variables are referenced inside methods. Third, 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
We could, if we wish, specify a default value for the distance in the usual way:

#shortnodeclass GoForward(float distance=100) : WalkNode($,_distance,0,0,1) : 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.

#ifndef _MySampleBehavior_h_
#define _MySampleBehavior_h_

#include "Behaviors/StateMachine.h"

#nodeclass MySampleBehavior : StateNode
  #shortnodeclass SayHello : SpeechNode($,"Hello")

  #shortnodeclass PlayGoodBye : SoundNode($,"crash.wav")

  #nodemethod setup
    #statemachine

      startnode: SayHello =T(5000)=> PlayGoodBye

    #endstatemachine

#endnodeclass
#endif

Advanced Concepts

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


Last modified: Tue Sep 8 04:47:40 EDT 2009