Tutorial: State Machine Signaling

From Tekkotsu Wiki

Jump to: navigation, search

Contents

Introduction

Transitions determine the flow of control in a state machine. They fire in response to events. These may be external events such as a button press, or they may be events generated by the nodes themselves, such as a completion event. A SpeechNode or SoundNode posts a completion event when it has finished playing its audio clip; a PostureNode posts a completion event when it has moved the robot's body to the specified posture. The completion event can trigger a CompletionTrans pointing from the node to the next node in the state sequence.

Completion events are a simple state signaling mechanism. In this tutorial you will learn (1) how nodes you define yourself can post completion events, (2) two additional forms of simple state signaling: success and failure events, and (3) a more complex form of state signaling that allows you to transmit data as part of the event.

Simple Signaling

Completion events are used when a node's action has completed normally. You can use the postStateCompletion() method to post a completion event at the end of your node's doStart(), or in other places if you're creating a more sophisticated node class.

Some types of nodes can also terminate abnormally, indicating that something went wrong. In that case they would post a failure event. For symmetry, there is also a success event, which might be used as an alternative to the completion event to indicate a positive outcome for a yes/no decision.

Signal TypeShorthandPosting Method
completion=C=>postStateCompletion()
success=S=>postStateSuccess()
failure=F=>postStateFailure()

Here is an example of a user-defined state node posting a completion event. This allows us to use a completion transition to sequence from this node to the next node:

$nodeclass CompletionExample : StateNode {

  $nodeclass PrintMessge(string const &msg) : StateNode : doStart {
    cout << "Message: " << msg << endl;
    postStateCompletion();
  }

  $setupmachine{
    start: PrintMessage("Listen for the ping.")  =C=>  SoundNode("ping.wav")
  }

}

If PrintMessage did not post a completion event, you would have to use a NullTrans or a TimeoutTrans instead of a CompletionTrans to sequence to the next node.

The PilotNode class includes a method called cancelThisRequest() that aborts the pilot request when called from within doStart(). The method also posts a failure event to indicate that the request has been canceled. One place you might use this cancellation mechanism is if the PilotNode wants to set up a request to navigate to a particular object on the world map, but it finds the object is missing from the map. A failure transition could be used to make the robot start a search for the object.

Signaling With Data

A completion event is the right thing to use if the only information a node conveys is "I'm done; move on to the next state." But sometimes we want the node to signal which of several paths the state machine should take, e.g., "turn left", "turn right", or "continue straight ahead". In order to do that, we use a SignalTrans: a templated class that works with any data type we want to signal with.

Signal TypeShorthandPosting Method
signal T=S<t>(value)=>
=S<t>=>
postStateSignal<T>(value)

Although we could use integers for signal values, it is better programming style to use an enumerated type, like this:

$nodeclass WallFollower : VisualRoutinesStateNode {

  enum Direction { turnLeft, turnRight, straightAhead };

  $nodeclass DecideNode : StateNode : doStart {
    ...
    if ( x < -10 ) {
      postStateSignal<Direction>(turnLeft);
      return;
    } else ...
  }

  $setupmachine{
    startnode: DecideNode
    startnode =S<Direction>(turnLeft)=> TurnBy(M_PI/4) =C=> startnode
    startnode =S<Direction>(turnRight)=> TurnBy(-M_PI/4) =C=> startnode
    startnode =S<Direction>(straightAhead)=> ForwardBy(100) =C=> startnode
  }

}

A SignalTrans with no value specified, e.g., =S<Direction>=>, acts like a default case: it will fire if no other SignalTrans matches the value that was signaled.

Receiving the Signal

When a node's doStart() is called due to an event triggering a transition into that node, the instance variable event is set to a pointer to that event. If the transition was a SignalTrans<T>, then the event will be a DataEvent<T>, and we can use the extractSignal<T>() method to read the value of the signal.

Building on the previous example, here is how we would extract the direction signal:

$nodeclass TurnBy(float angle) : PilotNode : doStart {
  Direction dir = extractSignal<Direction>(event);
  cout << "Received a signal with value " << (unsigned int)dir << endl;
  pilotreq.da = angle;
}

The extractSignal() method is especially useful when we want to signal an arbitrary value, such as a float, rather than one of a small set of enumeration values. In that case the signal transition omits the value, like this:

$nodeclass FloatSignal : StateNode {

  $nodeclass Transmitter : StateNode : doStart {
    float const randomValue = rand() / (float)RAND_MAX;
    postStateSignal<float>(randomValue);
  }

  $nodeclass Receiver : StateNode : doStart {
    float const value = extractSignal<float>(event);
    cout << "Received value " << value << endl;
  }

  $setupmachine{
    Transmitter =S<float>=> Receiver
  }

}

A Look At the Internals

Signaling events use the stateMachineEGID generator type. The source id is the address of the node sending the event, and the event type is statusETID. Thus, writing:

postStateCompletion();

is equivalent to writing:

erouter->postEvent(EventBase::stateMachineEGID, (size_t)this, EventBase::statusETID);

Including the node address as the source id is important because when multiple nodes are simultaneously active, if one of them completes, we only want to fire transitions coming from that node. If any other CompletionTrans is active, it will compare the source id of the completion event with the addresses of its source nodes and ignore any events that don't match.

State signaling is done using the DataEvent<T> event type and the stateSignalEGID generator id. Thus, writing:

postStateSignal<T>(value);

is equivalent to writing:

erouter->postEvent(DataEvent<T>(value, EventBase::stateSignalEGID, (size_t)this, EventBase::statusETID));

Success and failure events are actually signals using DataEvent<StateNode::SuccessOrFailure>.

The extractSignal<T>() method assumes we know the type of the DataEvent that was received. This is often the case in simple state machines, but it need not always be so. If a node is going to receive multiple types of events, it must check the event type before trying to extract data from it. One way to do this is to use the tryExtractSignal<T>() method, which returns a pointer to the data if the event is of the specified type, and NULL otherwise.

Signal TypeEvent TypeGenerator IDTransition TypeShorthandPosting Method
completionEventBasestateMachineEGIDCompletionTrans=C=>postStateCompletion()
successDataEvent<SuccessOrFailure>stateSignalEGIDSignalTrans<...>=S=>postStateSuccess()
failureDataEvent<SuccessOrFailure>stateSignalEGIDSignalTrans<...>=F=>postStateFailure()
signal TDataEvent<T>stateSignalEGIDSignalTrans<T>=S<t>(value)=>
=S<t>=>
postStateSignal<T>(value)

To fully understand the state signaling mechanism, you can read the source code in

  • /usr/local/Tekkotsu/Behaviors/StateNode.h and StateNode.cc
  • /usr/local/Tekkotsu/Behaviors/Transitions/SignalTrans.h

Exercises

Coming soon.