2017年8月1日 星期二

An scm framework tutorial II

  Hello and welcome to my scm library tutorial II. Today we will be studying how to use 'history' pseudo state and give you some usage advice.

  First, let's take a look of the source code as listed below. This statechart implemented some part of the example given in David Harel's paper, a citizen watch. I am not going to implement(convert) all of statecharts as(to) scxml from the paper, just some part of it is enough for you to get good understanding.

#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;
}

  The '2_sec' event should be enqueEvent()ed to the machine by your program after 2 seconds. Here, as example we simply enqueue all events. You can utilize StateMachine's registerTimedEvent() to schedule it like this:
    mach_->registerTimedEvent(2.0, "2_sec", false);
Note that the third argument, we passed in 'false' here, means mach_ should just do it (enqueue the '2_sec' event after 2 seconds) at the designated time no mater what happened to me(the caller) in the mean time. If you give it a 'true', you should preserve the returned pointer (and retain it). When the time comes the machine check if there's external references, if there's none, it won't do the enqueuEvent(). Thus, you can "cancel" it bye release the pointer before time's up.

  OK, let's check what's defined in the scxml. Observe the 'update' state, there's a line <history id='histu' type='shallow'/> . If you had studied the scxml's specification, you should know that a state with history set will remember in which substate it was in last time, when the machine come back to the state again it will enter the substate but not the default initial state. Here, suppose you are in 'hr' state currently, and you press the 'c' button, the machine will leave 'hr' state and leave 'update' state and then enter the 'time' state. When next time the machine come back to 'update' state (by holding 'c' button for more then 2 seconds in 'time' state), it will enter 'hr' state but not 'sec'(which is default substate for 'update' since it's the first substate it defined in scxml).

  There's two types of history: 'shallow' and 'deep'. 'shallow' will only remember the current substate you are in, the first layer. 'deep' will remember every substates, deep into the state tree,  it's like add history attributes in every substates, spare you from lots of typing. 'stopwatch' state is a good example, thanks to 'deep', wee don't need to pollute it with lots of 'history' attributes.

  You usually set the history attribute if have a state which need to remember its history, as we see in the 'stopwatch' state, you use xml's attribute, like <state id='stopwatch' history='deep'> . But in some scenario you may want to give the history an id, this way you can set a transition's target to the history. Like we see in 'update' state, when you are in 'update' state and you press the 'd' button, the machine will exit current state and make a transition to the 'histu' state, which is not really a state but update's history (we call it pseudo state). Enter the 'histu' state means entering the substate 'update' previously in, which is the substate you are leaving! Thus you leave but enter the same state immediately. StateMachine has a method re_enter_state() you can use to check if you are enter the same state you leave. Here we use it to check whether we should advance the value of hour or minute or second you are currently adjusting. I am considering to rename re_enter_state() to is_reenter_state(). What do you think about it? You are welcome to leave a comment to tell me.

  Not in the example, but you can make a state forget it's history by 'clh(theState)' usually in a 'ontransit' action.

  Last time we learnt that we can use macro REGISTER_STATE_SLOT to register actions for a state's onentry, onexit. The macro REGISTER_STATE_SLOT use setActionSlot to create mappings between 'onentry_xxx', 'onexit_xxx' and the boost binded object. You see we use setActionSlot in TheMachine's constructor to create the mappings. Because we simply want to display the state we are entering or leaving (utilizing getEnterState()), there's actually nothing specific, so we use a loop to loop through all states and set them all the same method. It is much easier than writing tens of line of REGISTER_STATE_SLOT. And then we overwrite the state that need to do things differently. Remember that we are just creating mappings here, you can always change the mapping before StartEngine().

  I hope this tutorial is sufficient to show you that it's simple and easy to use scm library. 

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是如何的簡單易用。我們下次見!

2017年7月22日 星期六

A scm framework tutorial (StateChart Machine)

Today, we will be learning how to use scm library。

First, you can down scm from here.

For those who don't know why we are using scm, please read the discussions in previous blogs.

There's some test programs in scm. This candy machine is one of them, listed below:

#include <StateMachineManager.h>
#include <iostream>

using namespace std;
using namespace SCM;

std::string cm_scxml = "\
   <scxml> \
        <state id='idle'> \
            <transition event='empty' target='disabled'/> \
            <transition event='coin' target='active'/> \
        </state> \
        <state id='active'> \
            <transition event='release-candy' ontransit='releaseCandy' target='releasing'/> \
            <transition event='withdraw-coin' ontransit='withdrawCoins' target='idle'/> \
        </state> \
        <state id='releasing'> \
            <transition event='candy-released' cond='condNoCandy' target='disabled'/> \
            <transition event='candy-released' target='idle'/> \
        </state> \
        <state id='disabled'> \
            <transition event='add-candy' cond='condNoCredit' target='idle'/> \
            <transition event='add-candy' target='active'/> \
        </state> \
    </scxml> \
";

class TheCandyMachine : public Uncopyable
{
    StateMachine *mach_;
    int           credit_; // 
    int           num_of_candy_stored_;
    
public:
    TheCandyMachine()
    : credit_(0)
    , num_of_candy_stored_(0)
    {
        mach_ = StateMachineManager::instance()->getMach("cm_scxml");
        REGISTER_STATE_SLOT (mach_, "idle", &TheCandyMachine::onentry_idle, &TheCandyMachine::onexit_idle, this);
        REGISTER_STATE_SLOT (mach_, "active", &TheCandyMachine::onentry_active, &TheCandyMachine::onexit_active, this);
        REGISTER_STATE_SLOT (mach_, "releasing", &TheCandyMachine::onentry_releasing, &TheCandyMachine::onexit_releasing, this);
        REGISTER_STATE_SLOT (mach_, "disabled", &TheCandyMachine::onentry_disabled, &TheCandyMachine::onexit_disabled, this);

        //boost::function<bool() const> cond_slot;
        REGISTER_COND_SLOT(mach_, "condNoCandy", &TheCandyMachine::condNoCandy, this);
        REGISTER_COND_SLOT(mach_, "condNoCredit", &TheCandyMachine::condNoCredit, this);
        
        // boost::function<void()> 
        REGISTER_ACTION_SLOT(mach_, "releaseCandy", &TheCandyMachine::releaseCandy, this);
        REGISTER_ACTION_SLOT(mach_, "withdrawCoins", &TheCandyMachine::withdrawCoins, this);
        
        mach_->StartEngine();
} ~TheCandyMachine () { mach_->ShutDownEngine(true); } void store_candy (int num) { num_of_candy_stored_ += num; mach_->enqueEvent("add-candy"); cout << "store " << num << " gumballs, now machine has " << num_of_candy_stored_ << " gumballs." << endl; } void insertQuater () { insert_coin(25); cout << "you insert a quarter, now credit = " << credit_ << endl; } void ejectQuater () { mach_->enqueEvent("withdraw-coin"); cout << "you pulled the eject crank" << endl; } void turnCrank () { mach_->enqueEvent("release-candy"); cout << "you turned release crank" << endl; } protected: void insert_coin (int credit) { credit_ += credit; mach_->enqueEvent("coin"); } void onentry_idle () { cout << "onentry_idle" << endl; cout << "Machine is waiting for quarter" << endl; if (num_of_candy_stored_ == 0) { mach_->enqueEvent ("empty"); } } void onexit_idle () { cout << "onexit_idle" << endl; } void onentry_active () { cout << "onentry_active" << endl; } void onexit_active () { cout << "onexit_active" << endl; } void onentry_releasing () { cout << "onentry_releasing" << endl; //WorkerManager::instance()->perform_work_after(1.0, boost::bind(&TheCandyMachine::candy_released, this), false); candy_released (); } void onexit_releasing () { cout << "onexit_releasing" << endl; } void onentry_disabled () { cout << "onentry_disabled" << endl; } void onexit_disabled () { cout << "onexit_disabled" << endl; } bool condNoCandy () const { return num_of_candy_stored_ == 0; } bool condNoCredit () const { return credit_ == 0; } void releaseCandy () { int num_to_release = credit_ / 25; if (num_to_release > num_of_candy_stored_) { num_to_release = num_of_candy_stored_; } cout << "release " << num_to_release << " gumballs" << endl; num_of_candy_stored_ -= num_to_release; credit_ -= num_to_release * 25; } void withdrawCoins () { cout << "there you go, the money, " << credit_ << endl; credit_ = 0; cout << "Quarter returned" << endl; } void candy_released () { mach_->enqueEvent("candy-released"); } public: void report () { cout << "\nA Candy Selling Machine\n"; cout << "Inventory: " << num_of_candy_stored_ << " gumballs\n"; cout << "Credit: " << credit_ << endl << endl; } void init () { mach_->frame_move(0); assert (mach_->inState("disabled")); this->store_candy(5); mach_->frame_move(0); assert (mach_->inState("idle")); report (); } void frame_move () { mach_->frame_move(0); } void test () { this->insertQuater(); frame_move(); this->turnCrank(); frame_move(); report (); this->insertQuater(); frame_move(); this->ejectQuater(); frame_move(); report (); this->turnCrank(); frame_move(); report (); this->insertQuater(); frame_move(); this->turnCrank(); frame_move(); this->insertQuater(); frame_move(); this->turnCrank(); frame_move(); this->ejectQuater(); frame_move(); report(); this->insertQuater(); frame_move(); this->insertQuater(); frame_move(); this->turnCrank(); frame_move(); this->insertQuater(); frame_move(); this->turnCrank(); frame_move(); this->insertQuater(); frame_move(); this->ejectQuater(); frame_move(); report(); this->store_candy(5); frame_move(); this->turnCrank(); frame_move(); report(); } }; int main(int argc, char* argv[]) { AutoReleasePool apool; StateMachineManager::instance()->set_scxml("cm_scxml", cm_scxml); { TheCandyMachine mach; mach.init (); mach.test (); } return 0; }


Please build it by using cmake, you need boost and expat library. If you are on Linux, there must be packages provided, just install them. If you are on Windows, you may need to compile expat yourself. We are using the header only part of boost, no compile is needed, just set the include path right. Don't know how to use cmake? Simply do the following in scm directory you clone out:

mkdir build
cd build
cmake ../scm
make

You should be done. Give it a run after the build complete, you will see outputs like following:

=========================================================
onentry_idle 
Machine is waiting for quarter
onexit_idle
onentry_disabled
store 5 gumballs, now machine has 5 gumballs.
onexit_disabled
onentry_idle
Machine is waiting for quarter

A Candy Selling Machine
Inventory: 5 gumballs
Credit: 0

you insert a quarter, now credit = 25
onexit_idle
onentry_active
you turned release crank
onexit_active
release 1 gumballs
onentry_releasing
onexit_releasing
onentry_idle
Machine is waiting for quarter

A Candy Selling Machine
Inventory: 4 gumballs
Credit: 0

you insert a quarter, now credit = 25
onexit_idle
onentry_active
you pulled the eject crank
onexit_active
there you go, the money, 25
Quarter returned                                                                                                                                                           
onentry_idle
Machine is waiting for quarter

A Candy Selling Machine
Inventory: 4 gumballs
Credit: 0

you turned release crank

A Candy Selling Machine
Inventory: 4 gumballs
Credit: 0

you insert a quarter, now credit = 25
onexit_idle
onentry_active
you turned release crank
onexit_active
release 1 gumballs
onentry_releasing
onexit_releasing
onentry_idle
Machine is waiting for quarter
you insert a quarter, now credit = 25
onexit_idle
onentry_active
you turned release crank
onexit_active
release 1 gumballs
onentry_releasing
onexit_releasing
onentry_idle
Machine is waiting for quarter
you pulled the eject crank

A Candy Selling Machine
Inventory: 2 gumballs
Credit: 0

you insert a quarter, now credit = 25
onexit_idle
onentry_active
you insert a quarter, now credit = 50
you turned release crank
onexit_active
release 2 gumballs
onentry_releasing
onexit_releasing
onentry_disabled
you insert a quarter, now credit = 25
you turned release crank
you insert a quarter, now credit = 50
you pulled the eject crank

A Candy Selling Machine
Inventory: 0 gumballs
Credit: 50

store 5 gumballs, now machine has 5 gumballs.
onexit_disabled
onentry_active
you turned release crank
onexit_active
release 2 gumballs
onentry_releasing
onexit_releasing
onentry_idle
Machine is waiting for quarter

A Candy Selling Machine
Inventory: 3 gumballs
Credit: 0

onexit_idle
===============================================================


OK, let's start from main() function to see how to use it.

First, you need to create an AutoReleasePool object. The way scm do memory management was follow Objective-C's. Since we are not really an Objective-C runtime but C++, we have to allocate an auto release pool ourself. The objects you invoke autorelease() on are actually put in the pool waiting to be release.

scm was developed for real time applications like games. In this type of applications there's usually a main loop keep running. You have to call the frame_move() of the FrameMovers objects telling it as argument how much time has passed.

The next statement


    StateMachineManager::instance()->set_scxml("cm_scxml", cm_scxml);


map and scxml id to it's content. There should be various state machines in your app, you will usually load their definitions when app start. Then when you need to produce a state machine object, simply doing it like this

    mach_ = StateMachineManager::instance()->getMach("cm_scxml");

invoke the getMach() method of StateMachineManager. cm_scxml is defined in souce code, you may want to store it in external space and load them in when needed.

Then, after you create the state machine object but before you StartEngine(), you will set some name/slots mappings. (We might talk about signal/slots in the future) There's three types of slot: action slot, condition slot, and frame move slot. scm invoke corresponding action/slot you set in scxml by their names. For your convenience, scm provided macros like REGISTER_STATE_SLOT() and REGISTER_COND_SLOT(). After the setup, call StartEngine().

After machine start, you simply use enqueEvent() to tell it what event happened. Later, when the machine's frame_move() called, it will handle those events, doing state transitions, invoke the slots you set, just like we see in the example code.

Isn't it easy? :D

Next week, we will be talking about the difference between scm and SCXML standard, and some points you have to be careful when using it.

scm framework 教學(StateChart Machine)

今天,我們來學習怎麼使用scm library。

首先,你可以在這裏下載scm。

不知道為什麼要用scm的請看之前blog的討論。

scm裏面有幾個測試程式,其中一個是糖果販賣機,原始碼如下:

#include <StateMachineManager.h>
#include <iostream>

using namespace std;
using namespace SCM;

std::string cm_scxml = "\
   <scxml> \
        <state id='idle'> \
            <transition event='empty' target='disabled'/> \
            <transition event='coin' target='active'/> \
        </state> \
        <state id='active'> \
            <transition event='release-candy' ontransit='releaseCandy' target='releasing'/> \
            <transition event='withdraw-coin' ontransit='withdrawCoins' target='idle'/> \
        </state> \
        <state id='releasing'> \
            <transition event='candy-released' cond='condNoCandy' target='disabled'/> \
            <transition event='candy-released' target='idle'/> \
        </state> \
        <state id='disabled'> \
            <transition event='add-candy' cond='condNoCredit' target='idle'/> \
            <transition event='add-candy' target='active'/> \
        </state> \
    </scxml> \
";

class TheCandyMachine : public Uncopyable
{
    StateMachine *mach_;
    int           credit_; // 
    int           num_of_candy_stored_;
    
public:
    TheCandyMachine()
    : credit_(0)
    , num_of_candy_stored_(0)
    {
        mach_ = StateMachineManager::instance()->getMach("cm_scxml");
        REGISTER_STATE_SLOT (mach_, "idle", &TheCandyMachine::onentry_idle, &TheCandyMachine::onexit_idle, this);
        REGISTER_STATE_SLOT (mach_, "active", &TheCandyMachine::onentry_active, &TheCandyMachine::onexit_active, this);
        REGISTER_STATE_SLOT (mach_, "releasing", &TheCandyMachine::onentry_releasing, &TheCandyMachine::onexit_releasing, this);
        REGISTER_STATE_SLOT (mach_, "disabled", &TheCandyMachine::onentry_disabled, &TheCandyMachine::onexit_disabled, this);

        //boost::function<bool() const> cond_slot;
        REGISTER_COND_SLOT(mach_, "condNoCandy", &TheCandyMachine::condNoCandy, this);
        REGISTER_COND_SLOT(mach_, "condNoCredit", &TheCandyMachine::condNoCredit, this);
        
        // boost::function<void()> 
        REGISTER_ACTION_SLOT(mach_, "releaseCandy", &TheCandyMachine::releaseCandy, this);
        REGISTER_ACTION_SLOT(mach_, "withdrawCoins", &TheCandyMachine::withdrawCoins, this);
        
        mach_->StartEngine();
} ~TheCandyMachine () { mach_->ShutDownEngine(true); } void store_candy (int num) { num_of_candy_stored_ += num; mach_->enqueEvent("add-candy"); cout << "store " << num << " gumballs, now machine has " << num_of_candy_stored_ << " gumballs." << endl; } void insertQuater () { insert_coin(25); cout << "you insert a quarter, now credit = " << credit_ << endl; } void ejectQuater () { mach_->enqueEvent("withdraw-coin"); cout << "you pulled the eject crank" << endl; } void turnCrank () { mach_->enqueEvent("release-candy"); cout << "you turned release crank" << endl; } protected: void insert_coin (int credit) { credit_ += credit; mach_->enqueEvent("coin"); } void onentry_idle () { cout << "onentry_idle" << endl; cout << "Machine is waiting for quarter" << endl; if (num_of_candy_stored_ == 0) { mach_->enqueEvent ("empty"); } } void onexit_idle () { cout << "onexit_idle" << endl; } void onentry_active () { cout << "onentry_active" << endl; } void onexit_active () { cout << "onexit_active" << endl; } void onentry_releasing () { cout << "onentry_releasing" << endl; //WorkerManager::instance()->perform_work_after(1.0, boost::bind(&TheCandyMachine::candy_released, this), false); candy_released (); } void onexit_releasing () { cout << "onexit_releasing" << endl; } void onentry_disabled () { cout << "onentry_disabled" << endl; } void onexit_disabled () { cout << "onexit_disabled" << endl; } bool condNoCandy () const { return num_of_candy_stored_ == 0; } bool condNoCredit () const { return credit_ == 0; } void releaseCandy () { int num_to_release = credit_ / 25; if (num_to_release > num_of_candy_stored_) { num_to_release = num_of_candy_stored_; } cout << "release " << num_to_release << " gumballs" << endl; num_of_candy_stored_ -= num_to_release; credit_ -= num_to_release * 25; } void withdrawCoins () { cout << "there you go, the money, " << credit_ << endl; credit_ = 0; cout << "Quarter returned" << endl; } void candy_released () { mach_->enqueEvent("candy-released"); } public: void report () { cout << "\nA Candy Selling Machine\n"; cout << "Inventory: " << num_of_candy_stored_ << " gumballs\n"; cout << "Credit: " << credit_ << endl << endl; } void init () { mach_->frame_move(0); assert (mach_->inState("disabled")); this->store_candy(5); mach_->frame_move(0); assert (mach_->inState("idle")); report (); } void frame_move () { mach_->frame_move(0); } void test () { this->insertQuater(); frame_move(); this->turnCrank(); frame_move(); report (); this->insertQuater(); frame_move(); this->ejectQuater(); frame_move(); report (); this->turnCrank(); frame_move(); report (); this->insertQuater(); frame_move(); this->turnCrank(); frame_move(); this->insertQuater(); frame_move(); this->turnCrank(); frame_move(); this->ejectQuater(); frame_move(); report(); this->insertQuater(); frame_move(); this->insertQuater(); frame_move(); this->turnCrank(); frame_move(); this->insertQuater(); frame_move(); this->turnCrank(); frame_move(); this->insertQuater(); frame_move(); this->ejectQuater(); frame_move(); report(); this->store_candy(5); frame_move(); this->turnCrank(); frame_move(); report(); } }; int main(int argc, char* argv[]) { AutoReleasePool apool; StateMachineManager::instance()->set_scxml("cm_scxml", cm_scxml); { TheCandyMachine mach; mach.init (); mach.test (); } return 0; }


先用cmake編譯起來吧,你需要boost與expat這兩個library。如果你使用Linux,系統一定有提供package。如果是使用Windows,可能要自己build expat。boost方面只要header only的boost signals與boost function,設定一下include路徑就行了。不知道怎麼用cmake嗎?大概像這樣,先到clone出來的目錄,然後

mkdir build
cd build
cmake ../scm
make

就行了。build完執行看看吧,你應該可以見到如下的輸出:
=========================================================
onentry_idle
Machine is waiting for quarter
onexit_idle
onentry_disabled
store 5 gumballs, now machine has 5 gumballs.
onexit_disabled
onentry_idle
Machine is waiting for quarter

A Candy Selling Machine
Inventory: 5 gumballs
Credit: 0

you insert a quarter, now credit = 25
onexit_idle
onentry_active
you turned release crank
onexit_active
release 1 gumballs
onentry_releasing
onexit_releasing
onentry_idle
Machine is waiting for quarter

A Candy Selling Machine
Inventory: 4 gumballs
Credit: 0

you insert a quarter, now credit = 25
onexit_idle
onentry_active
you pulled the eject crank
onexit_active
there you go, the money, 25
Quarter returned                                                                                                                                                            
onentry_idle
Machine is waiting for quarter

A Candy Selling Machine
Inventory: 4 gumballs
Credit: 0

you turned release crank

A Candy Selling Machine
Inventory: 4 gumballs
Credit: 0

you insert a quarter, now credit = 25
onexit_idle
onentry_active
you turned release crank
onexit_active
release 1 gumballs
onentry_releasing
onexit_releasing
onentry_idle
Machine is waiting for quarter
you insert a quarter, now credit = 25
onexit_idle
onentry_active
you turned release crank
onexit_active
release 1 gumballs
onentry_releasing
onexit_releasing
onentry_idle
Machine is waiting for quarter
you pulled the eject crank

A Candy Selling Machine
Inventory: 2 gumballs
Credit: 0

you insert a quarter, now credit = 25
onexit_idle
onentry_active
you insert a quarter, now credit = 50
you turned release crank
onexit_active
release 2 gumballs
onentry_releasing
onexit_releasing
onentry_disabled
you insert a quarter, now credit = 25
you turned release crank
you insert a quarter, now credit = 50
you pulled the eject crank

A Candy Selling Machine
Inventory: 0 gumballs
Credit: 50

store 5 gumballs, now machine has 5 gumballs.
onexit_disabled
onentry_active
you turned release crank
onexit_active
release 2 gumballs
onentry_releasing
onexit_releasing
onentry_idle
Machine is waiting for quarter

A Candy Selling Machine
Inventory: 3 gumballs
Credit: 0

onexit_idle
===============================================================


我們從 main()開始看看怎麼使用吧。

首先你需要生出一個AutoReleasePool 的object。scm學Objective-C的方式管理記憶體,但我們畢竟不是一個Objective-C的runtime,而是C++,所以至少要自己生成一個 auto release pool物件。你呼叫autorelease()的物件其實會被放在這邊等待被release。

scm是為遊戲類的即時應用開發的,這類的即時應用通常會有一個main loop,你需要在main loop的每個frame呼叫我們FrameMover物件的frame_move(),參數是經過了多少時間。

接下來這行

    StateMachineManager::instance()->set_scxml("cm_scxml", cm_scxml);

將一個scxml id與scxml內容關聯起來。你的應用裏不會只有一個state machine,通常你會在程式開始時將state machine的定義load進來。這樣的話當要生成一個state machine時只要像這樣

    mach_ = StateMachineManager::instance()->getMach("cm_scxml");

呼叫getMach()就行。cm_scxml是定義在原始碼裏的字串,你可能會想把這些state machine的定義存在外部空間,需要時再load進來。

接下來,在生成新state machine之後,在呼叫它的StartEngine之前,你會先設定一些名字與slot的對應(改天再來談signal/slot吧)。scm裏分三種slot: action slot, condition slot, 與 frame move slot。你的scxml裏設定的onentry, onexit, ontransit等action就是呼叫你設定的slot。為方便使用,scm提供REGISTER_STATE_SLOT(), REGISTER_COND_SLOT()等巨集。設定好之後就可以StartEngine()了。

接下來你只要用enqueEvent()來告訴mach發生了什麼事件,之後mach在 frame_move時就會處理這些事件,進行狀態轉換,並呼叫你設定好的slot,就像我們在範例看到的。

是不是很簡單易用呢。:D

下週我們再來談scm與SCXML標準不同的地方和使用時的一些注意事項吧。

2017年7月16日 星期日

About states, part III

In last blog I talked about how state pattern is not enough for real application development if the states are quite complex. Even if you finished the implementation you would put in a lot of effort into maintenance. So, what can you do?

We talked about state transition and hierarchical and parallel states. For better understanding, you should read a paper published over 30 years ago, David Harel, statecharts. You should read it at least once. Read it, and come back here, I will be waiting here.

Long time ago, I was so frustrating on how difficult it is to manage states in program until I found this paper via google about 10 years ago. I read it and think to myself, that's it! If there's a library offer the statecharts functionality, it will be great! It certainly will enable us to develop better software and make it much easier to maintain.

To my surprise, I couldn't find a C++ library offer this functionality. As a result, I wrote one myself. Till these days, it's still one of the best things I ever developed.

Although it's most intuitive as human beings are good at grasping graphical charts, it is not so for a computer. You have to translate it to texts so computer can process it. There's a specification developed by W3C named SCXML in XML format. Please familiarize yourself with them from the official side.

    3.2 <scxml>
    3.3 <state>
    3.4 <parallel>
    3.5 <transition>
    3.6 <initial>
    3.7 <final>
    3.8 <onentry>
    3.9 <onexit>
    3.10 <history>

My idea is to use the format defined in scxml except the executable and control flow part, since we are using it with a programming language. It would be just like a black box. By using the signal/slots mechanism you simply connect to the signals it triggered when the onentry, onexit, or ontransit happened. You basically only invoked it's enqueEvent() and frame_move() methods. In stead of make many function calls, It's much easier to use by making it a framework, avoiding handling the trivia. Although you will have to do thing following it's rule.

Well, later I found out that the Boost libraries offer not only one but two libraries that support state machines: boost Meta State Machine and boost statechart. They use jargon defined in UML, there's difference with what Harel or SCXML used. I personally don't like it. You could try them out to see if you like either one, maybe you will find one suit your need. It seems they were not present when I start looking for a state machine solution. But even if they were, I would still develop one myself though, they are simply not what I wanted. If you are writing your own version, consider follow the scxml format like me. Although something like JSON may be more to your liking, but I think it's a good thing to have a standard.

Code utilize my library looks like this:
static string app_scxml =
"<scxml initial='init'>"
"   <state id='init'>"
"       <transition event='login' target='online'/>"
"   </state>"
"   <state id='online'>"
"       <state id='idle'>"
"           <transition event='hmi-connected' target='use'/>"
"       </state>"
"       <state id='use'>"
"           <transition event='hmi-disconnected' cond='condNoConnection' target='idle'/>"
"       </state>"
"   </state>"
"   <transition event='logout' target='offline'/>"
"   <final id='offline'/>"
"</scxml>"
;


struct AppServiceHandler
{
    // member declarations
    . 
    .
    .
    wtk::Net::StateMachine                  *mach_;
    
    AppServiceHandler()
    : (0)
    .
    .
    .
    {
        
        mach_ = StateMachineManager::instance()->getMach("app_scxml");
        REGISTER_STATE_SLOT (mach_, "init", &AppServiceHandler::onentry_init, &AppServiceHandler::onexit_init, this);
        REGISTER_STATE_SLOT (mach_, "online", &AppServiceHandler::onentry_online, &AppServiceHandler::onexit_online, this);
        REGISTER_STATE_SLOT (mach_, "idle", &AppServiceHandler::onentry_idle, &AppServiceHandler::onexit_idle, this);
        REGISTER_STATE_SLOT (mach_, "use", &AppServiceHandler::onentry_use, &AppServiceHandler::onexit_use, this);
        REGISTER_STATE_SLOT (mach_, "offline", &AppServiceHandler::onentry_offline, &AppServiceHandler::onexit_offline, this);
        
        boost::function<bool()> cond_slot;
        mach_->setCondSlot("condNoConnection", cond_slot = boost::bind(&AppServiceHandler::PRIVATE::condNoConnection, this));
        mach_->StartEngine();
    }
    
    ~AppServiceHandler()
    {
        mach_->ShutDownEngine(true);
    }
    
    // member functions
    .
    .
    .
    // 
    void onentry_init ();
    void onexit_init ();
    void onentry_online ();
    void onexit_online ();
    void onentry_idle ();
    void onexit_idle ();
    void onentry_use ();
    void onexit_use ();
    void onentry_offline ();
    void onexit_offline ();
    bool condNoConnection () const;
    

Observe that you can define the statcharts as a string in one place. In stead of spread the states information all over the place and define the states relationship all over the place. It is much easier to understand and later to maintain.

Below is an scxml definition for one of a game component I developed previously.

<scxml initial="alive">
 <state id="spawn">
  <transition event="play" target="alive"/>
 </state>
 
 <parallel id="alive">
  <transition event="die" target="dead"/>
  
  <state id="gun">
   <transition event="gun" target="gun_on"/>
   <state id="gun_off">
   </state>
   <state id="gun_on">
    <state id="gun_reload">
     <transition event="gun_reloaded" target="gun_fire"/>
    </state>
    <state id="gun_fire">
     <transition event="no_bullet" target="gun_off"/>
    </state>
   </state>
  </state>
  
  <state id="wave">
   <transition event="wave" target="wave_on"/>
   <state id="wave_off">
   </state>
   <state id="wave_on">
    <transition event="no_wave" target="wave_off"/>
   </state>
  </state>
  
  <state id="radar">
   <transition event="radar" target="radar_on"/>
   <state id="radar_off">
   </state>
   <state id="radar_on">
    <transition event="radar_off" target="radar_off"/>
   </state>
  </state>
  
  <state id="shield">
   <state id="shield_off">
    <transition event="shield" target="shield_on"/>
   </state>
   <state id="shield_on">
    <transition event="collide_dot" target="shield_explode"/>
    <transition event="collide_boss" target="shield_explode"/>
   </state>
   <state id="shield_explode">
    <transition event="shield" target="shield_on"/>
    <transition event="shield_off" target="shield_off"/>
   </state>
  </state>
  
  <state id="phantom">
   <transition event="phantom" target="phantom_on"/>
   <state id="phantom_off">
   </state>
   <state id="phantom_on">
    <transition event="phantom_off" target="phantom_off"/>
   </state>
  </state>
  
  <state id="explode">
   <transition event="explode" cond="cond_FB_enabled" ontransit="spawnExplode" target="firebird_on"/>
   <transition event="explode" ontransit="spawnExplode" target="explode_on"/>
   <state id="explode_off">
   </state>
   <state id="explode_on">
    <transition event="explode_off" target="explode_off"/>
   </state>
   <state id="firebird_on">
    <transition event="firebird_off" target="explode_off"/>
   </state>
  </state>
  
  
  
  <state id="display">
   <transition event="shielding_on" cond="!In(firebirding)" target="shielding"/>
   <transition event="phantom_on" cond="!In(firebirding)" target="phantoming"/>
   <transition event="firebird_on" cond="!In(firebirding)" target="firebirding"/>
   <state id="normal">
   </state>
   <state id="shielding">
    <transition event="shielding_off" cond="In(phantom_on)" target="phantoming"/>
    <transition event="shielding_off" target="normal"/>
   </state>
   <state id="phantoming">
    <transition event="phantom_off" cond="In(shield_explode)" target="shielding"/>
    <transition event="phantom_off" target="normal"/>
   </state>
   <state id="firebirding">
    <transition event="firebird_off" cond="In(phantom_on)" target="phantoming"/>
    <transition event="firebird_off" cond="In(shield_explode)" target="shielding"/>
    <transition event="firebird_off" target="normal"/>
   </state>
  </state>
 </parallel>
 
 <state id="dead">
  <transition event="play" target="alive"/>
 </state>
</scxml>

Intimidating, right? If it's not that I utilized the statechart library. I can't image how difficult it would be to debug or trace code if I have to found all the states information all over the place.

I will later upload this scm library to github, you will find it here, stay tuned.

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...