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.

關於狀態(states), part III

上次我們談到使用state pattern的支巧不足以對付實際開發的需求,只是套用state pattern的方法在維護上將付出很大的代價,所以該如何是好呢?

我們談到了狀態的轉換(transition)還有階層式與平行狀態等,這裏有篇David Harel, statecharts在數十年前寫的paper,你一定要讀一次,去讀完它,我等你。

很久很久以前我對於怎麼處理程式中狀態的問題非常傷腦筋,直到我大概十年前google到了這篇paper,讀完後覺得豁然開朗,心想如果有提供這種功能的library可以用就太好了,如果我們能善加利用支援statechart功能的library,一定能寫出更好使用與維護的程式碼。

讓我疑惑的是,似乎找不到C++的library,結果我自己寫了一套。直到今天還是我開發的東西中最好用的東西之一。

用圖型表示雖然最直覺,人類能很快懂,但是要使用在電腦程式裏當然還是得變成文字才方便。W3C定義了SCXML, 用xml的格式來表達。
重點的這些元件還請上官網去熟悉一下

    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>

我的構想就是使用跟scxml定義的一樣的格式但是不實作它定的executable的功能,把它當一個黑盒子一樣,它用signal/slot的機制讓你去connect onentry, ontransit, onexit等時候發出的signal。控制它基本上只會用到enqueEvent()與frame_move()。用一般的方式只是一些function叫來叫去的話不是很好用,我們需要framework來幫我們處理很多鎖事,儘管使用的話就得遵守framework的規則。

後來我發現boost library中有兩個library提供state machine的功能,boost Meta State Machineboost statechart。不過它們使用的術語是UML中定義的,與原本Harel使用的不同,個人實在不欣賞。你可以學著使用看看,或許合你味口也說不定。當初似乎還沒這兩個library,不過即使是現在我應該也會選擇--自己寫一個,建議如果你也想寫一套的話,照WC3的spec來開發,有一個共同的標準是好事。

我個人寫的scm的framework, 使用起來像這樣

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;
    

注意到我們直接用scxml的格式把state machine定義好,相對於此,你在boost chart或一些其它的library中,各個state的定義與關係的建立通常散落在各處程式碼中。那樣實在不好理解與維護。

下面是我以前開發的一個遊戲中角色的scxml

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

有沒有覺得很可怕?如果不是使用這一套statechart的方式,而是讓狀態的資訊散落在程式中,不敢想像維護起來要花多大的功夫。

稍後我將上傳我寫作的scm library到github, 你可以由這裏取得。

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