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. 

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