状態機械

前のページ
目次
次のページ


このページの内容


状態機械(State Machines)

 ロボットに複雑な動きをさせるときに使う、最も一般的な方法は状態機械(state machine)を定義する方法です。状態機械は、いくつかの「状態」と「状態間の遷移」で構成されます。状態は、あるアクションを実行します。遷移は、ある状態から別の状態への移動のことで、センサーからのイベントに応じて遷移します(下図)。

 Tekkotsu は状態機械を実現するために、StateNode(状態ノード)Transition(遷移) という2つのクラスを用意しています。これらクラスは BehaviorBaseのサブクラスです。

 1つの状態は、そのDoStart()関数により呼び出されて起動します。この起動が次に StateNode::DoStart() を呼び、さらに StateNode::DoStart() が、その状態につながる全ての遷移の DoStart()関数を呼びます(下図)。それぞれの遷移は、いろいろなタイプのイベントリスナーを設定します。特定のタイプのイベントを受け取ると、遷移が起動します。遷移は、前の状態のDoStop()関数を呼ぶことにより、前の状態の活動を止め、次の状態のDoStart()関数を呼ぶことにより、次の状態を起動します。前の状態が止まると、その状態から出ている遷移も、それらのDoStop()関数により全て止まり、それらが持つリスナーが削除されます。

 下図は、3個のノードで構成される簡単な状態機械です。この状態機械は、状態Bark からスタートします(Bark=吠える)。Barkには2本の出力遷移があります(出力遷移=出て行く矢印)。1本は「Head button pressed(頭ボタンが押された)」というイベントのリスナーを設定します。もう1本は「5 second timer expires(タイマーが5秒経過した)」というイベントのリスナーを設定します。  起動すると、Barkは吠え声を再生し、AIBOを座らせます。そしてイベントが起こるのを待ちます。AIBOの頭ボタンが押されると、ping.wavサウンドを再生して Wait状態に遷移します。Bark状態のときに、5秒以内にボタンが押されないと、タイマーが時間切れになり、Howl状態に遷移します(howl=遠吠え)。ボタンが押されたときも、押されずに5秒が経過したときも、Bark状態は止まり、Barkから出る2つの遷移も止まります。Howl状態は、howl.wavを再生し、再生が完了したイベント(howl completed)を投げます。このイベントが Howl から Wait への遷移を起動します。Wait状態は何もしませんが、タイマーが15秒経過するのを持ちます。タイマーが時間切れになると、この遷移が起こり、Wait状態から Bark状態へ戻ります。

 Tekkotsuの状態機械の構造は再帰的です。つまり、どの状態も、別の状態機械全体を含めることができます。

状態機械としてビヘイビアを定義するための一般的な手順は次の通りです。

 例えば "SampleState" のような名前を付けて1つのStateNodeを作ります。この親ノードで、setup()関数を提供し、この関数内で状態機械に必要な全てのノードと遷移のインスタンスを作ります。
 次に、SampleState の DoStart()関数を呼び、状態機械のスタート・ノードを起動します。そして、スタート・ノードは出力遷移を起動します。

 重要なのは、setup()とDoStart()の違いを理解することです。状態機械が動くとき、いろいろなノードと遷移が DoStart()と DoStop()関数を持ちます。しかし、ノードや遷移の活動が止まるとき、それらが消えるわけではありません。それらは存在し、再び活動を始めるかもしれません。状態機械を使い、それを捨てたいとき、親ノードの teardown()関数を呼びます。各StateNode は子ノードのリストを持ち、それぞれの子ノードが遷移のリストを持ちます。teardownは全てのノードと遷移が消えるまで再帰的に進みます。

練習: Bark/Howl 状態機械

 状態機械を作るために次の2つのファイルをインクルードします。
  Behaviors/StateNode.h
  Behaviors/Transition.h

 さらに、使用するサブクラスのファイルをインクルードします。
 StateNode のサブクラスは、Behaviors/Nodes に含まれます。
 Transition のサブクラスは、Behaviors/Transitions に含まれます。

#ifndef INCLUDED_SampleState_h_
#define INCLUDED_SampleState_h_
 
#include "Behaviors/StateNode.h"
#include "Behaviors/Transition.h"
#include "Behaviors/Nodes/SoundNode.h"
#include "Behaviors/Transitions/CompletionTrans.h"
#include "Behaviors/Transitions/EventTrans.h"
#include "Behaviors/Transitions/TimeOutTrans.h"
#include "Events/EventRouter.h"

 さて、SampleState はStateNode の子クラスです。 StateNode は BahaviorBase の子クラスです。スタート・ノードへのポインタを作るために、protected変数を使用します。この場合のスタート・ノードは、bark_node です。

class SampleState : public StateNode {
protected:
  StateNode *startnode;

public:
  SampleState() : StateNode("SampleState"), startnode(NULL) {}

 setup() 関数は、状態機械を構築します。この関数は、SampleStateのDoStart()関数が最初に呼ばれるときに自動的に呼ばれます。ローカル変数 bark_node、howl_node、wait_node は、setup()が再び呼ばれると捨てられます。しかし、これらノードへのポインタは、addNode()を呼ぶことにより、StateNode自身が維持します。また、これら3ノードは、出力遷移へのポインタを維持します。

  virtual void setup() {

    // ここは決まりごと //
    StateNode::setup();
    std::cout << getName() << " is setting up the state machine." << std::endl;

    // ノードを作成 //
    SoundNode *bark_node = new SoundNode("bark","barkmed.wav");
    SoundNode *howl_node = new SoundNode("howl","howl.wav");
    StateNode *wait_node = new StateNode("wait");

    // ノードを追加 //
    addNode(bark_node);
    addNode(howl_node);
    addNode(wait_node);

    // 遷移を作成 //
    EventTrans *btrans = new EventTrans(wait_node,EventBase::buttonEGID,
                                        RobotInfo::HeadFrButOffset,EventBase::activateETID);
    btrans->setSound("ping.wav");

    // 遷移を追加 //
    bark_node->addTransition(btrans);
    bark_node->addTransition(new TimeOutTrans(howl_node,5000));
    howl_node->addTransition(new CompletionTrans(wait_node));
    wait_node->addTransition(new TimeOutTrans(bark_node,15000));

    // スタート・ノードを指定 //
    startnode = bark_node;
  }

 DoStart() は状態機械を起動したいときに呼ばれます。DoStart() は最初に StateNode::DoStart() を呼びます。それから、その子ノードの1つのDoStart()を呼んで状態機械をスタートします。
 DoStart() は同様に StateNode::DoStop() を呼びます。下の方の privateセクションは、コンパイラのwarning(警告)を避けるために必要なダミーのコードです。クラスがポインターデータの変数を持つときに書きます。ここでのポインターデータ変数は、startnode です。

  virtual void DoStart() {
    StateNode::DoStart();
    std::cout << getName() << " is starting up." << std::endl;

    startnode->DoStart();
  }
 
  virtual void DoStop() {
    std::cout << getName() << " is shutting down." << std::endl;
    StateNode::DoStop();
  }



private:  // コンパイル時の警告を避けるためのダミー関数
  SampleState(const SampleState&);
  SampleState& operator=(const SampleState&);

};

#endif

秀丸を開き、上記プログラムをコピー&貼り付け、次の通り保存してください。

    保 存 先 : マイドキュメント → usXX → project
    ファイル名 : SampleState.h
    ファイル種類: C言語ヘッダーファイル(*.h)

ファイルを保存したら、次の手順で実行してください。

構文

  • ノードを作成するためのコンストラクタ StateNode
    StateNode *node = new StateNode (const std::string & nodename);

  • サウンドノードを作成するためのコンストラクタ SoundNode
    SoundNode *node = new SoundNode ( const std::string & nodename,
                             const std::string & soundfilename);

  • ノードを追加するためのメソッド addNode
    addNode( StateNode *node );      

  • 遷移を作成するためのコンストラクタ EventTrans
    EventTrans *trans = new EventTrans ( StateNode *destination,
                             EventBase::EventGeneratorID_t gid,
                             unsigned int sid,
                             EventBase::EventTypeID_t tid);
       その他のEventTransについては、こちら
  • 遷移を追加するためのメソッド addTransition
    node->addTransition ( Transition *trans );      

  • "ノードの活動が完了したときに実行する遷移"を作成するためのコンストラクタ CompletionTrans
    node->addTransition ( new CompletionTrans ( StateNode *destination) );

  • "タイムアウトになったときに実行する遷移"を作成するためのコンストラクタ CompletionTrans
    node->addTransition ( new TimeOutTrans ( StateNode *destination
                                 unsigned int delay) );
        delay の単位はミリ秒

Event Logging

 状態ノードは活動を開始するときと停止するとき、常に stateMachineEGIDイベントを投げます。このイベントには sourceIDとeventTypeID がついています。その sourceIDは 状態ノードのアドレスです。eventTypeID は activateETID または deactivateETIDです。

さらに、状態ノードがモーションコマンドや音声再生のようなアクションを実行する場合、状態ノードが投げるイベントタイプは statusETID です。

また、遷移が起こるときは常に、状態ノードは stateTransitionEGID 状態イベントを投げます。これらのイベントは Tekkotsuの event logger を使用して観察できます。

Event Logger の使用

  1. 上記の状態機械のプログラムをコンパイルしてください。

  2. コンパイルしたプログラムをAIBOに入れ、AIBOを起動してください。

  3. Cygwin画面を開き、下記のように打ち込んで、AIBOに無線接続してください。

    us0X@xxxxx ~
    
    $ telnet AIBOのIPアドレス 59000      
    
  4. コントローラ画面のメニューから次の順に選んでください。
       Root Control > Status Reports > Event Logger
       stateMachineEGID と stateTransitionEGID をダブルクリックしてください。
       それらの横にチェックマークが表示されます。

  5. Root Control > 0. Mode Switch に戻ってください。
       SampleState を起動してください。
       Telnet画面に一連のイベントが表示されるでしょう。
       最初のイベントは、SampleState が起動したことによるものです。
       次は、barkノードの起動によるイベントです。
       このイベントは、ボタンを押したか、タイマーの時間切れのどちらかにより発生します。

  6. Event Logger に戻ってください。
       buttonEGID と audioEGID をダブルクリックして、チェックを表示します。
       これで、状態機械を動かすと、2つの状態の変遷の引き金となるボタンと音声のイベントをTelnet画面で見ることができます。

 Telnet画面に表示されるイベントの例を示します。

SoundManager::LoadBuffer() of 8836 bytes                      音声ファイル読み込み
EVENT: (audioEGID,/ms/data/sound/ping.wav,A)                  音声ファイル ping.wav 再生
EVENT: (stateTransitionEGID,{bark}--EventTrans-->{wait},S)    bark - イベント遷移 -> wait
EVENT: (stateMachineEGID,bark,D)                              bark 停止
EVENT: (stateMachineEGID,wait,A)                              wait 開始
EVENT: (buttonEGID,HeadBut,A)                                 ボタン押下開始
EVENT: (buttonEGID,HeadBut,S)                                 ボタン押下中
EVENT: (buttonEGID,HeadBut,D)                                 ボタン押下停止
EVENT: (stateTransitionEGID,{wait}--TimeOutTrans-->{bark},S)  wait - タイムアウト遷移 -> bark 
EVENT: (stateMachineEGID,wait,D)                              wait 停止
EVENT: (stateMachineEGID,bark,A)                              bark 開始
SoundManager::LoadBuffer() of 8836 bytes                      音声ファイル読み込み
EVENT: (audioEGID,/ms/data/sound/barkmed.wav,A)               音声ファイル barkmed.wav 再生 
EVENT: (stateTransitionEGID,{bark}--TimeOutTrans-->{howl},S)  bark - タイムアウト遷移 -> howl 
表示されるイベントの順序が直感に反するかもしれません。
例えば、ボタン押下に対する反応です。
最初に EventTrans(イベント遷移)が表示され、次に barkノードの活動停止、waitノードの活動開始、そしてこれらの遷移の引き金になるボタン押下イベントの順になるからです。
この順序はイベントロギング・アルゴリズムの結果で、あなたが書くプログラムには影響しません。

 実際の各イベントの時刻を見るには、メニューで Event Logger へ行き、下までスクロールダウンしてください。
Verbosity項目をクリックすると、"Verbosity (0)" を読むことができます。
右側の Send Input 欄で、1をタイプし、キーボードのEnterキーを押してください。
すると、メニューには "Verbosity(1)" が表示されます。
イベントログの各行は2種類の数値を含みます。これらは、持続時間と時刻です。

新ノードタイプの定義

 Tekkotsu が提供するノードの種類は少しです。シンプルな効果を出すだけなら、それらノードを利用できます。しかし、一般的には、必要とする機能を達成するためには自分で新しいノードタイプを定義しなければならないことがあります。新しいノードタイプを定義するには、すでにあるノードタイプのソースコードを見れば可能です。実は、上で作ってきたSampleState は StateNode から派生したクラスなのです。

 新しい状態ノードクラスを作ることを考えましょう。
その状態ノードクラスの名前を PlayNTimesNode とします。
このクラスは音を何度か矢継ぎ早に鳴らすものとします。
これを実現するには、このクラスをSoundNodeのサブクラスにし、それから音声ファイルのコピーを出力バッファに追加するのにChainFile機能を使用します。
親クラス SoundNodeのソースコードは
SoundNode.hです。
SoundNodeから PlayNTimesNode を構築するには、次の項目を追加します。
  ・再生の反復回数を示す変数(int ntimes)
  ・これらの反復をつなぐDoStart()関数

 以下にPlayNTimesNodeのソースを示します。
 これを次の通り保存してください。

    保 存 先 : マイドキュメント → usXX → project
    ファイル名 : PlayNTimesNode.h
    ファイル種類: C言語ヘッダーファイル(*.h)


#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 DoStart() {
    SoundNode::DoStart();

    // 反復
    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

 ここで、3回吠える状態機械を示します。
"barkmed.wav"を3回再生する PlayNTimesNode を用います。
そして、5秒待って繰り返します。
これは、自身へ遷移する状態ノードの例でもあります。

 それから、setup()関数で音声ファイルを前もってロードするという仕掛けを用います。
こうすることにより、繰り返してロードする必要がなくなります。
状態機械が消滅するとき、teardown()関数を使って音声ファイルを解放します。

 さきほど作成した SampleState.h に次のプログラムを上書きしてください。

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

#include "PlayNTimesNode.h"

class SampleState : public StateNode {
private:
  StateNode* startnode;
public:
  SampleState() : StateNode("SampleState"), startnode(NULL) {}

  virtual void setup() {
    StateNode::setup();

    // 音声ファイルをロード
    sndman->LoadFile("barkmed.wav");
    // woofノードを作成
    StateNode* woof_node = new PlayNTimesNode("woof","barkmed.wav",3);
    // woofノードを追加
    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 DoStart() {
    StateNode::DoStart();
    std::cout << getName() << " is starting up." << std::endl;
    startnode->DoStart();
  }
 
  virtual void DoStop() {
    std::cout << getName() << " is shutting down." << std::endl;
    StateNode::DoStop();
  }

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

};

#endif

PlayNTimesNode が2つのコンストラクタをもつ理由(ここは理解できなくてもOK)
 状態ノードと遷移を持つTekkotsuビヘイビアは全て、クラス名とインスタンス名の両方を持ちます。通常、インスタンス名は第1引数としてコンストラクタに渡されます。もし、インスタンス名が供給されないと、クラス名がデフォルトのインスタンス名として供給されます。また、サブクラスになるビヘイビアは2つの文字列引数(サブクラス名とインスタンス名)を取る2番目のコンストラクタを提供しなければなりません。 このフォームは、サブクラスがこのクラスから継承されるときだけ使用されて、親のコンストラクタをそれが呼ぶとき特定のクラス名を供給する必要があります。この第2コンストラクタは、publicではなく、protectedで定義され、サブクラスからのみアクセス可能です。

 SoundNodeサブクラスとして PlayNTimesNode を定義するときは、これらの決まりに従いました。PlayNTimesNode コンストラクタは、1個の文字列引数を取り、SoundNodeのコンストラクタの2つの文字列を呼び、クラス名として"PlayNTimesNode"を渡します。第2引数は利用者が与えるインスタンス名です。

 PlayNTimesNodeのサブクラスを後で作りたいと思うかもしれないので、2つの文字列という形式でコンストラクタが提供されました。このコンストラクタは定義するどのサブクラスでも使われるでしょう。

新しい遷移タイプ

 新しいタイプの遷移を定義することは、新しいタイプのノードを定義する場合に似ています。
 継承するための親クラスを取り出し、変化させるべき項目だけをオーバーライドします。
 できるだけ親クラスのメソッドを使用します。
 もし、前もって定義された遷移タイプの中に適切な親クラスがなければ、親として一般的なTransitionクラスを使います。

LostTargetTrans

 ピンクボールを追跡していて、一定時間、ボールを見失ったら、ある状態から遷移したい場合を考えましょう。
これを実現するには、TimeOutTransのサブクラスとしてLostTargetTransを定義します。
タイマーが時間切れになる前にピンクボールが見えれば、遷移はvisionイベントを読み、タイマーを元に戻すでしょう。
画像ノイズによる間違いを防ぐために、タイマーが戻るまで少なくとも5フレームはターゲットを見なければなりません。

 遷移は名前を必要としませんが、遷移先のノードを必要とします。 このため、2つのpublicコンストラクタを与える必要があります。 1つは第1引数文字列(遷移に対するインスタンス名)、もう1つは引数なしです。 さらに、遷移の新しいクラスが自身のサブクラスを持つことができるなら、3番目のコンストラクタを与える必要があります。 このコンストラクタは2つの文字列引数(クラス名とインスタンス名)を持ちます。

#ifndef INCLUDED_LostTargetTrans_h_
#define INCLUDED_LostTargetTrans_h_

#include "Behaviors/Transitions/TimeOutTrans.h"

class LostTargetTrans : public TimeOutTrans {
 public:

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

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

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


  virtual void processEvent(const EventBase &e) {
    if (e.getGeneratorID()==EventBase::visObjEGID && e.getSourceID()==sid) {
      ++counter;
      if (counter > minf) resetTimer();
    }
    else
      TimeOutTrans::processEvent(e);
  }

  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 delay, int minframes=5) :
    TimeOutTrans(classname,instancename,destination,delay),
    sid(source_id), minf(minframes), counter(0) {}

 private:
  unsigned int sid;
  int minf;    // タイマーが戻るまでにターゲットが写るべきフレーム数
  int counter; // ターゲットが写っているフレーム数
};

#endif

 LostTargetTransの定義において、明示的にfire()メソッドを呼んでいません。
もし(タイマーが時間切れになり)タイマーイベントが受け取られたら、単純にそれをTimeOutTrans::processEvent()に渡し、遷移が始まるように仕向けるだけです。

発展:

  1. LostTargetTrans.h を利用して、AIBOが5秒間ピンクボールを見失ったら、いつもbarkサウンドを再生する SampleState.h を作成してください。
    soruce id として ProjectInterface::visPinkBallSID に渡す必要があります。
    例は
    Vision ObjectイベントとText Message イベントの節を見てください。

  2. LostTargetTransのDoStart()関数内に次の赤い行を追加してください。

      virtual void DoStart() {
        TimeOutTrans::DoStart();
        std::cout << getName() << std::endl;
        erouter->addListener(this,EventBase::visObjEGID,sid);
      }
    

    getName()メソッドは起こっている遷移の名前を調べます。
    プログラムをAIBOで動かし、Telnet画面に表示されるメッセージを確認してください。

    
    {woof}--LostTargetTrans-->{woof}
    

    一般にgetName()を使用すると、遷移の様子は次の形式で表示されます。

    
    {src1,src2,...}--TransitionClassName-->{dest1,dest2,...}
    

      ここで、src1, src2, ...は遷移元、
      TransitionClassName は遷移クラス名、
      dest1, dest2, ...は遷移先です。

発展1の解答


前のページ
目次
次のページ