2017年7月30日 星期日

scm framework 教學之二

  你好,我們又見面了。今天我們來研究怎麼使用scm的history及一些注意事項。

  首先我們還是先把程式碼列出來看看吧,這是一個citizen錶的部分statechart,取自Harel的paper。我無意把David Harel使用的citizen錶的所有statechart都轉成scxml,想必只使用其中一部分大家就能舉一反三。

#include <StateMachineManager.h>
#include <uncopyable.h>

#include <iostream>

using namespace std;
using namespace SCM;

std::string watch_scxml = "\
   <scxml non-unique='on,off'> \
       <state id='time'> \
            <transition event='a' target='alarm1'/> \
            <transition event='c_down' target='wait'/> \
       </state> \
       <state id='wait'> \
            <transition event='c_up' target='time'/> \
            <transition event='2_sec' target='update'/> \
       </state> \
       <state id='update'> \
            <history id='histu' type='shallow'/> \
            <transition event='d' target='histu'/> \
            <state id='sec'> \
                <transition event='c' target='1min'/> \
            </state> \
            <state id='1min'> \
                <transition event='c' target='10min'/> \
            </state> \
            <state id='10min'> \
                <transition event='c' target='hr'/> \
            </state> \
            <state id='hr'> \
                <transition event='c' target='time'/> \
            </state> \
       </state> \
       <state id='alarm1' history='shallow'> \
            <transition event='a' target='alarm2'/> \
            <state id='off' > \
                <transition event='d' target='on'/> \
            </state> \
            <state id='on' > \
                <transition event='d' target='off'/> \
            </state> \
       </state> \
       <state id='alarm2' history='shallow'> \
            <transition event='a' target='chime'/> \
            <state id='off' > \
                <transition event='d' target='on'/> \
            </state> \
            <state id='on' > \
                <transition event='d' target='off'/> \
            </state> \
       </state> \
       <state id='chime' history='shallow'> \
            <transition event='a' target='stopwatch'/> \
            <state id='off' > \
                    <transition event='d' target='on'/> \
            </state> \
            <state id='on' > \
                <transition event='d' target='off'/> \
            </state> \
       </state> \
       <state id='stopwatch' history='deep'> \
            <transition event='a' target='time'/> \
            <state id='zero'> \
                <transition event='b' target='on,regular'/> \
            </state> \
            <parallel> \
                <state id='run'> \
                    <state id='on' > \
                        <transition event='b' target='off'/> \
                    </state> \
                    <state id='off' > \
                        <transition event='b' target='on'/> \
                    </state> \
                </state> \
                <state id='display'> \
                    <state id='regular'> \
                        <transition event='d' cond='In(on)' target='lap'/> \
                        <transition event='d' cond='In(off)' target='zero'/> \
                    </state> \
                    <state id='lap'> \
                        <transition event='d' target='regular'/> \
                    </state> \
                </state> \
            </parallel> \
       </state> \
    </scxml> \
";


class TheMachine : public Uncopyable
{
    StateMachine *mach_;
    int hr_;
    int min_;
    int sec_;
    
public:
    TheMachine()
    : hr_(0)
    , min_(0)
    , sec_(0)
    {
        mach_ = StateMachineManager::instance()->getMach("watch");
        mach_->retain();
        vector<string> const&states = mach_->get_all_states();
        cout << "we have states: " << endl;
        for (size_t state_idx=0; state_idx < states.size(); ++state_idx) {
            State *st = mach_->getState(states[state_idx]);
            if (st == 0) continue; // <- state machine itself
            for (int i=0; i < st->depth(); ++i) {
                cout << "    ";
            }
            cout << states[state_idx] << endl;
            mach_->setActionSlot ("onentry_" + states[state_idx], boost::bind (&TheMachine::onentry_report_state, this, false));
            mach_->setActionSlot ("onexit_" + states[state_idx], boost::bind (&TheMachine::onexit_report_state, this));
        }
        cout << endl;
        mach_->setActionSlot ("onentry_sec", boost::bind (&TheMachine::onentry_sec, this));
        mach_->setActionSlot ("onentry_1min", boost::bind (&TheMachine::onentry_1min, this));
        mach_->setActionSlot ("onentry_10min", boost::bind (&TheMachine::onentry_10min, this));
        mach_->setActionSlot ("onentry_hr", boost::bind (&TheMachine::onentry_hr, this));
        mach_->StartEngine();
    }
    
    ~TheMachine ()
    {
        mach_->release();
    }
    
    void onentry_sec()
    {
        if (mach_->re_enter_state()) {
            sec_ = 0;
        }
        onentry_report_state(true);
    }
    
    void onentry_1min()
    {
        if (mach_->re_enter_state()) {
            ++min_;
        }
        onentry_report_state(true);
    }
    
    void onentry_10min()
    {
        if (mach_->re_enter_state()) {
            min_ += 10;
        }
        onentry_report_state(true);
    }
    
    void onentry_hr()
    {
        if (mach_->re_enter_state()) {
            ++hr_;
        }
        onentry_report_state(true);
    }
    
    void onentry_report_state(bool with_time)
    {
        ++sec_;
        cout << "enter state " << mach_->getEnterState()->state_uid();
        if (with_time) {
            cout << ". time " << hr_ << ":" << min_ << ":" << sec_;
        }
        cout << endl;
    }
    
    void onexit_report_state()
    {
        cout << "exit state " << mach_->getEnterState()->state_uid() << endl;
    }
    
    void test ()
    {
        // time
        mach_->enqueEvent("c_down"); // -> wait
        mach_->enqueEvent("2_sec"); // -> update, you will use registerTimedEvent to generate event after 2 seconds
        mach_->enqueEvent("d"); //  reset, 1 second
        mach_->enqueEvent("d"); //  reset, 1 second
        mach_->enqueEvent("d"); //  reset, 1 second
        mach_->enqueEvent("c"); // -> 1min state
        mach_->enqueEvent("d"); //  1 min
        mach_->enqueEvent("d"); //  2 min
        mach_->enqueEvent("c"); // -> 10min state
        mach_->enqueEvent("d"); //  12 min
        mach_->enqueEvent("c"); // -> hr state
        mach_->enqueEvent("d"); //  1 hr
        mach_->enqueEvent("d"); //  2 hr
        mach_->enqueEvent("c"); // -> time
        
        mach_->enqueEvent("a"); // -> alarm1
        mach_->enqueEvent("d");
        mach_->enqueEvent("a"); // -> alarm2
        mach_->enqueEvent("d");
        mach_->enqueEvent("a"); // -> chime
        mach_->enqueEvent("d");
        mach_->enqueEvent("a"); // -> stopwatch.zero
        mach_->enqueEvent("b"); // -> run.on
        mach_->enqueEvent("d"); // -> display.lap
        mach_->enqueEvent("a"); // -> time
        mach_->enqueEvent("d"); // no effect
        mach_->enqueEvent("a"); // -> alarm1
        mach_->enqueEvent("d");
        mach_->enqueEvent("a"); // -> alarm2
        mach_->enqueEvent("d");
        mach_->enqueEvent("a"); // -> chime
        mach_->enqueEvent("d");
        mach_->enqueEvent("a"); // -> stopwatch, what's run and display in?
        
        mach_->frame_move(0);
    }
};

int main(int argc, char* argv[])
{
    AutoReleasePool apool;
    StateMachineManager::instance()->set_scxml("watch", watch_scxml);
    {
        TheMachine mach;
        mach.test ();
    }
    return 0;
}

  那個 '2_sec' 的event是你的程式在經過2秒之後再enqueEvent()進去的,雖然應該很清楚這邊只是因為範例的關係我們才這樣寫,怕你誤會還是說一聲。使用StateMachine的話可以用registerTimedEvent()來達成喲,像這樣:
    mach_->registerTimedEvent(2.0, "2_sec", false);
注意第三個參數,如果你給它true的話,你要保存它傳回的指標(加上retain),當時間到的時候系統會根據該物件是不是還有外部reference來決定要不要執行enqueEvent的動作。如此,你可以有機會「取消」該動作,只要在時間到之前先release保留的指標就可免除event被發出來。

  好,我們回頭看看scxml的定義。請看'update' state的部分,它有一行<history id='histu' type='shallow'/> 的設定。如果你己經研究了scxml的spec的話,應該知道有設定history的state在機器重新回到該state時會進入原本所在的substate。所以像這邊如果你原本在'hr'的substate,然後按了'c'鍵,系統將會離開'update' state, 進入'time' state。當下次系統再次回到'update' state時(在'time' state下按住'c'鍵超過兩秒),機器將會進入'hr' state,儘管我們定義'update'的初始substate是'sec'(當沒有設定'initial'時,第一個定義的substate就是該state的initial state)。

  history的種類有兩種:'shallow'與'deep'。'shallow'只會記一層的歷史,'deep'的話會記下所有階層substate的歷史,省得你每一層都去設定history,如我們在'stopwatch'這個state所見,可以少打很多字呢。

  一般來說你要設定一個state使用history, 只要用attributes的方式就行了,就像<state id='stopwatch' history='deep'> 但是有些情況你可能希望能給history一個id, 這樣你就可以指定transition的目標到該history。就像'update' state裏那樣,'d'的event發生時,機器會轉到'histu'這個'假'state,histu實際上是上次機器所在的state,所以在這邊的效果就是機器會離開目前的state然後馬上再次進入該state。StateMachine有個re_enter_state()可以檢查離開的state是不是與進入的state相同,如果相同我們就把目前正在調整的時、分、或秒的數值加1。我正在考慮把re_enter_state()改名叫 is_reenter_state(),你覺得如何呢?歡迎在下面留下comment告訴我。

  在範例中雖然沒有,但是你可以用'clh(StateName)'的方式來清除一個state的history,通常會放在 transition的的 'ontransit' 中。

  我們上次有學到可以使用巨集REGISTER_STATE_SLOT來注冊一個state的onentry與onexit動作。如果你看過REGISTER_STATE_SLOT的定義的話應該就知道我們是用setActionSlot的方式來建立'onentry_xxx','onexit_xxx'與boost bind物件的對應。我們在TheMachine的constructor裏使用這種方式來建立對應,因為我們只是要顯示進入與離開的state是什麼(用getEnterState()),所以用一個for loop把所有的對應都設成一樣的,這樣就不用寫幾十行的REGISTER_STATE_SLOT了。然後再覆蓋掉要特別處理的幾個state就行了。記得這只是建立對應的動作,在StartEngine()前都是可以變動的。


  好了,希望借由這個範例你能了解scm是如何的簡單易用。我們下次見!

沒有留言:

張貼留言

ftps.space released!

  I started doing web site development some time ago.   After fiddled with Flask, nginx, javascript, html, css, websocket, etc, etc... I h...