Exploring Tekkotsu Programming on Mobile Robots:

Defining New Types of Nodes and Transitions

Prev: State signaling
Up: Contents
Next: ---

Contents: New node types, PlayNTimesNode, New transition types, LostTargetTrans, Nested state machines

Defining New Node Types

Tekkotsu provides only a few built-in node types. Although they can be composed to produce some simple effects (by using multiple-source and multiple-destination transitions, as shown above), in general you should expect to construct your own node types to achieve the functionality you require. Fortunately, this is not hard. You can look at the source code for the built-in node types to see how to do it. They all inherit from StateNode. We've actually been doing this all along, since DstBehavior is a class that inherits from StateNode.

[ Define a child of MapBuilderNode to look for markers. Also show how to use VRmixin to allow an ArmNode to look in localShS. ] Suppose we want to create a new state node class called PlayNTimesNode that plays a sound several times in rapid succession. We can easily do this by making it a subclass of SoundNode, and using the chainFile function to append additional copies of the sound file to the output buffer. First, look at the source code for SoundNode, which you can find in Behavior/Nodes/SoundNode.h. To construct PlayNTimesNode from SoundNode, all we need to add is a variable to hold the number of repetitions to play, and a version of start() that chains these repetitions together. Here is the source for PlayNTimesNode, which should go in the file PlayNTimesNode.h. The purpose of the second constructor function will be explained shortly. The variable curplay_id is inherited from SoundNode.

#ifndef INCLUDED_PlayNTimesNode_h_
#define INCLUDED_PlayNTimesNode_h_

#include "Behaviors/Nodes/SoundNode.h"

class PlayNTimesNode : public SoundNode {
protected:
  int ntimes;

public:
  PlayNTimesNode(std::string nodename="PlayNTimesNode", std::string soundfilename="",
		 int _ntimes=1) : 
    SoundNode("PlayNTimesNode",nodename,soundfilename), ntimes(_ntimes) {}

  virtual void start() {
    SoundNode::start();
    for (int i=2; i<=ntimes; i++)
      sndman->chainFile(curplay_id,filename);
  };

  void setNTimes(int const n) { ntimes = n; }

protected:
  PlayNTimesNode(std::string &classname, std::string &nodename, std::string &soundfilename,
		 int _ntimes=1) : 
    SoundNode(classname,nodename,soundfilename), ntimes(_ntimes) {}

};

#endif

Here is a little state machine that goes "woof, woof, woof": it uses PlayNTimesNode to repeat the "barkmed.wav" sound three times. Then it waits 5 seconds and repeats. This is an example of a state node that transitions back to itself.

We'll also use the trick of pre-loading the sound file in the setup() function, so that it doesn't have to be loaded repeatedly. We can release the sound file when the state machine is destroyed, by adding a teardown() function.

#ifndef INCLUDED_DstBehavior_h_
#define INCLUDED_DstBehavior_h_
 
#include "Behaviors/StateNode.h"
#include "Behaviors/Nodes/SoundNode.h"
#include "Behaviors/Transitions/TimeOutTrans.h"

#include "PlayNTimesNode.h"

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

  virtual void setup() {
    StateNode::setup();
    sndman->LoadFile("barkmed.wav");
    StateNode* woof_node = new PlayNTimesNode("woof","barkmed.wav",3);
    addNode(woof_node);
    woof_node->addTransition(new TimeOutTrans(woof_node,5000));
    startnode = woof_node;
  }

  virtual void teardown() {
    sndman->releaseFile("barkmed.wav");
    StateNode::teardown();
  }

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

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

};

Why does PlayNTimesNode have two constructors? All Tekkotsu behaviors, including state nodes and transitions, have both a class name and an instance name. Normally the instance name is passed as the first argument to the constructor. If an instance name is not supplied, the class name serves as a default instance name. Behaviors that are going to be subclassed must also provide a second constructor that takes two string arguments; a subclass name and an instance name. This form is only used when a subclass inherits from this class and needs to supply a more specific class name when it calls the parent's constructor. This second constructor is therefore declared "protected" rather than "public", so that it can only be accessed by subclasses.

We have followed these conventions in defining PlayNTimesNode as a subclass of SoundNode. The PlayNTimesNode constructor takes a single string argument and calls the two-string version of SoundNode's constructor, passing "PlayNTimesNode" as the class name. The second argument is the user-supplied instance name.

Since we may later want to create a subclass of PlayNTimesNode, we have also provided the two-string form of its constructor, which will be used by any subclasses we define.

Defining New Transition Types

Defining a new type of transition is similar to defining a new state node type. Pick a parent class to inherit from, and override only the things that need to change. Make use of the parent class's methods as much as possible. If none of the predefined transition types seems like an appropriate parent, use the generic Transition class as the parent.

LostTargetTrans

Suppose we are tracking the pink ball, and we want to transition out of a state if we lose sight of the ball for a certain period of time. We can do this by defining LostTargetTrans as a subclass of TimeOutTrans. The transition will subscribe to vision events and reset the timer if the desired target is seen before the timer expires. To avoid being fooled by noise in the image, we will require that the target by seen in at least 5 frames in order for the timer to be reset.

Transitions need not be given names, but they must always specify a destination node. Therefore we need to provide two public constructors, one with a string as first argument (the instance name for the transition), and one without. In addition, if this new class of Transition may have subclasses of its own, we need to provide a third constructor that takes two string arguments, one for the classname and one for the instance name.

class LostTargetTrans : public TimeOutTrans {
 public:

  LostTargetTrans(StateNode* destination, unsigned int source_id,
		  unsigned int timeLimit, int minframes=5) :
    TimeOutTrans("LostTargetTrans","LostTargetTrans",destination,timeLimit),
    sid(source_id), minf(minframes), counter(0) {}

  LostTargetTrans(const std::string &name, StateNode* destination, unsigned int source_id,
		  unsigned int timeLimit, int minframes=5) :
    TimeOutTrans("LostTargetTrans",name,destination,timeLimit),
    sid(source_id), minf(minframes), counter(0) {}

  virtual void start() {
    TimeOutTrans::start();
    erouter->addListener(this,EventBase::visObjEGID,sid);
  }

  virtual void doEvent() {
    if (event->getGeneratorID()==EventBase::visObjEGID && event->getSourceID()==sid) {
      ++counter;
      if (counter > minf) resetTimer();
    }
    else
      TimeOutTrans::doEvent();
  }

  virtual void resetTimer() {
    TimeOutTrans::resetTimer();
    counter = 0;
  }

  virtual void set_minframes(int minframes) { minf = minframes; }

protected:
  LostTargetTrans(const std::string &classname, const std::string &instancename, 
		  StateNode* destination, unsigned int source_id,
		  unsigned int timeLimit, int minframes=5) :
    TimeOutTrans(classname,instancename,destination,timeLimit),
    sid(source_id), minf(minframes), counter(0) {}

 private:
  unsigned int sid;
  int minf;   // number of frames that target must be seen before resetting the timer
  int counter; // number of frames target has been seen so far
};

Notice that we do not explicitly call fire() in the definition of LostTargetTrans. If a timer event is received (meaning the timer has expired), we simply pass it on to TimeOutTrans::processEvent() and let it take care of firing the transition.

Explore more:

  1. Write a variant of the one-node "woof" behavior above that uses LostTargetTrans to play a bark sound whenever the AIBO loses sight of the pink ball for 5 seconds. You will need to pass it ProjectInterface::visPinkBallSID as the source id; see the example in the Vision and text message events section of this tutorial.

  2. If a transition instance is not given an explicit name, it is assigned a default name of form {src1,src2,...}--TransitionClassName-->{dest1,dest2,...}. Define a subclass of LostTargetTrans in which you override the fire() method to print out a message whenever the transition fires, and then call the parent class's fire() method. The message should include a call to getName() to retrieve the name of the transition that is firing. Test your code by telnetting to port 59000 to see the message it prints.

[To be added: Defining RandomTrans to choose randomly from a set of destinations. ]

Nested State Machines

[To be written.] Return to parent. Return from parent.

Prev: State signaling
Up: Contents
Next: Storyboard tool


Last modified: Thu Jun 17 22:04:31 EDT 2010