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.

沒有留言:

張貼留言

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