Tutorial: Defining New Node Classes

From Tekkotsu Wiki

Jump to: navigation, search

Learning Goal: In this tutorial you will learn how to define new node classes (called derived classes) to extend the functionality of the built-in classes like StateNode or SpeechNode. This can be used to create more sophisticated robot behaviors.

Contents

Components of a Derived Node Class

Suppose we want to make a new kind of node that prints the message "Hello world!" on the robot's console. We need to specify two things to make a new node class. First, we have to choose a parent class for the node. In this example the best choice for a parent would be StateNode, since we don't need to borrow the functionality of a SoundNode, SpeechNode, or any of the other subclasses of StateNode that are built in to Tekkotsu. So our HelloNode will be a subclass of StateNode.

The second thing we need is a specification of how a HelloNode should behave. Most of the time all we care about is what should happen when the node is activated. This is controlled by the node's doStart() method. It's also possible to specify other aspects of a node's behavior, such as the constructor method, or a doStop() method, but we'll leave those for later. So here is how to write a HelloNode:

$nodeclass HelloNode : StateNode : doStart {
  cout << "Hello world!" << endl;
}

Here is an example of a state machine MyMachine1 incorporating the HelloNode:

#include "Behaviors/StateMachine.h"

$nodeclass MyMachine1 : StateNode {

  $nodeclass HelloNode : StateNode : doStart {
    cout << "Hello world!" << endl;
  }

  $setupmachine{
     HelloNode =N=> SoundNode("ping.wav")
  }

}

REGISTER_BEHAVIOR(MyMachine1);

As you can see from the above example, MyMachine1 has two parts. First we define all the derived node classes we wish to use, by putting $nodeclass{} calls inside the scope of the MyMachine1 definition. Here we're using the derived class HelloNode. Then we instantiate our derived class (and the built-in class SoundNode as well) in the $setupmachine{} portion of MyMachine1 to build a little two-node state machine.

Note: inside the $setupmachine, writing HelloNode without any argument list is the same as writing HelloNode().

Classes vs. Instances

It's important to understand the distinction between a node class, defined using the $nodeclass directive, and the instances of that node. Instances are defined by writing the node class name inside a $setupmachine block. You can have as many instances of a node class as you like. Each must have a unique label. If you don't label the instances yourself, the state machine compiler will create a label for you, such as statenode1 or speechnode7.

Suppose we wanted to make a behavior that prints "Hello world!" and plays a sound, and repeats this every five seconds. Here is some faulty code that does not achieve this:

HelloNode  =N=>  SoundNode("ping.wav")  =T(5000)=>  HelloNode

What's the problem? All we're doing is trying to write a simple loop, right? The problem is that the second occurrence of HelloNode makes a new instance; it does not cause the state machine to loop back to the first instance. Every time you write the name of a node class, you get a brand new instance. What we must do instead is assign a unique label to the first HelloNode and then transition back to that label, like this:

hello: HelloNode  =N=>  SoundNode("ping.wav")  =T(5000)=>  hello

To prevent confusion between node classes and node instances, it may be helpful to temporarily give up the convenience of chaining and instead instantiate each of your nodes on separate lines, then define all your transitions afterward, as shown below. If you adopt this discipline, your transitions will always be between labels, never between node class names:

hello: HelloNode
beep: SoundNode("beep.wav")

hello =N=> beep
beep =T(5000)=> hello

As you gain fluency with state machine shorthand you may naturally start to use chaining without getting confused about classes vs. instances.

It also helps to keep this convention in mind: node class names must begin with a capital letter. Instance names (labels) must begin with a lowercase letter. The state machine compiler will generate an error message if you try to violate this rule.

Classes With Multiple Methods

In the above example, HelloNode had only a single method, so we included the method name, doStart, as part of the $nodeclass line. When a node has multiple methods, we can't do that. Instead we must write a separate C++ style method declaration for each one. In the example below, the node has both a doStart() and a doStop() method. The doStop() method will be called when the node is deactivated.

$nodeclass MyMachine2 : StateNode {

  $nodeclass HelloNode : StateNode {

    virtual void doStart() {
      cout << "Hello world!" << endl;
    }

    virtual void doStop() {
      cout << "Bye now!" << endl;
    }

  }

  $setupmachine {
    HelloNode =T(5000)=> SoundNode("ping.wav")
  }

}

If you run the MyMachine2 behavior, it activates the HelloNode, whose doStart() method prints "Hello there!". Five seconds later, the timeout transition fires, which deactivates its source node, the HelloNode. This causes the HelloNode's doStop() method to run, and it prints "Bye now!". Then the transition activates its target node, the SoundNode, and the robot makes a ping sound.

Notice that MyMachine1 used a null transition =N=> while MyMachine2 uses a timeout transition =T(5000)=>. Either is fine. But we cannot use a completion transition =C=> here because HelloNode never posts a completion event. We'll learn when and how to post our own completion events in a later tutorial.

Specializing the Built-In Classes

Since StateNode is a built-in class, we've already done a bit of specializing of bult-in classes. But some of the other statenode types provide more interesting possibilities because you can manipulate their member variables. For example, SpeechNode has a member variable called textstream. If you write text to this stream, the node will speak it:

$nodeclass SayPiSquared : SpeechNode : doStart {
  texstream << "Pi squared is " << M_PI * M_PI;
}

Another example is the PilotNode, which has a member variable pilotreq of type PilotRequest. You can modify the fields of pilotreq in the doStart(), and when the doStart() returns, the request is automatically passed to the Pilot to execute. Here is how to define a node that tells the robot to move forward by 500 millimeters. Note that in this example, instead of just specifying a parent class name, PilotNode, we specify the complete constructor call; this allows us to pass arguments to the constructor.

$nodeclass Forward500 : PilotNode(PilotTypes::walk) : doStart {
   pilotreq.dx = 500;
}

Constructor Arguments

Your derived node classes can accept constructor arguments. The arguments are cached in member variables of the same name, and are accessible to all the class's methods, including doStart() and dStop(). Here is an example of a class that moves forward by a specified distance. We will pass the distance as an argument in the constructor call when we instantiate the class in $setupmachine{}.

$nodeclass MyMachine4 : StateNode {

  $nodeclass ForwardBy(float distance) : PilotNode(PilotTypes::walk) : doStart {
    pilotreq.dx = distance;
  }

  $setupmachine {
    ForwardBy(200) =T(3000)=> ForwardBy(-100) =T(3000)=> ForwardBy(50)
  }

}

Don't confuse constructor calls like ForwardBy(200) with function calls. There is no ForwardBy function. MyMachine4 is actually creating three separate instances of the ForwardBy class, i.e., three separate state nodes. Since we didn't assign explicit labels to them, the labels were generated automatically by the state machine parser. They are forwardby1, forwardby2, and forwardby3. Each instance has its own private data member named distance that stores the value that was supplied in the constructor call for that instance.

If you wish, you may supply default values for constructor parameters. For example, we could make the default forward travel distance be 50 millimeters:

$nodeclass ForwardBy(float distance=50) : PilotNode(PilotTypes::walk) : doStart {
  pilotreq.dx = distance;
}

You must follow the C++ conventions for default values, i.e., if a constructor parameter is given a default value, all the parameters that follow it must also have default values.

Node Name Constructor Argument

Take a look at the constructor definitions for StateNode and SpeechNode. Note that the first argument always specifies the name to be assigned to the new instance. This name is stored inside the instance as a string. It is not the same as the C++ variable your state machine will create inside the setup() method to hold a pointer to that instance: one is a string constant, while the other is a pointer variable. However, to keep things simple, it's best if the C++ variable name and the instance name always match. To ensure that this is the case, we never write the node name string directly; instead we allow the state machine compiler to insert the name for us.

When we define our own node classes using $nodeclass, we don't have to say that the first constructor argument is the instance name; the state machine compiler puts that in automatically. We simply list any additional constructor arguments we need, such as the distance argument in the ForwardBy node discussed earlier.

Test Your Understanding

1. The doStart() and doStop() methods are declared virtual. What does this mean? Where are these methods inherited from?

2. The default behavior of any state node when it becomes activated is to first run its doStart() method, then activate the start node of the state machine contained inside it, if there is one. So when you start MyMachine1 (e.g., from the ControllerGUI menu), it starts the HelloNode contained inside it. What would happen if you added a doStart() method to MyMachine1 that printed the message "Ahem..."? Write a version of MyMachine1 like this, and say what output you would expect to see from it.

3. In question 2 above, does it matter whether you put the doStart definition for MyMachine1 before or after the $setupmachine? Explain your reasoning.

4. Why won't the state machine below work as intended?

SpeechNode("start")  =C=>  HelloNode  =C=>  SpeechNode("end")

To Learn More

Now you know how to construct derived classes for the most common cases. There are additional options available for more complex situations, such as inheriting from multiple parents, or writing explicit initializers for the data members you define for a node class. We don't have a tutorial on these topics yet, but they are covered in the reference article on State machine shorthand: class definitions.

Exercises

1. Write a DoubleSpeak node that takes a string argument the way SpeechNode does, but says the argument twice, e.g., if given that argument "hello" it should say "hello hello". Make sure it functions correctly when given a word like "this" or "holes".

2. State machines can nest. Define a state machine named One that says the word "one" and then plays a dog bark sound, barkmed.wav. Define another state machine named Two that says the word "two" and then plays a sequence of two dog barks. Define the state machine Three analogously. Then put the definitions of the One, Two, and Three node classes inside a grandparent state machine called Count. The setupmachine{} for Count should invoke One, then Two, then Three; chain these together using timeout transitions to space them five seconds apart. Note that you cannot chain them with completion transitions because the parent state machines One, Two, and Three do not post completion events of their own. (Their children, which are SpeechNodes and SoundNodes, do post completion events, but the parents do not. You'll see in a later tutorial how to make a node or a state machine post completion events when you want them.)