Tomasz
Tomasz

Reputation: 21

Best way to common behaviour in state transition

This question is about boost::msm. I want to implement some base behaviour, for all state transition. What is the best way to execute some function on any state transition? There is some default mechanism in this state machine?

Upvotes: 2

Views: 71

Answers (1)

sehe
sehe

Reputation: 392833

I took the first sample I could find from https://www.boost.org/doc/libs/1_81_0/libs/msm/doc/HTML/examples/Orthogonal-deferred.cpp and extracted the on_enter/on_exit behaviours:

using boost::core::demangle;
template <typename Derived> struct SharedBehaviours : public msm::front::state<> {
    void on_entry(auto const& /*evt*/, auto& /*fsm*/) { std::cout << "entering: " << demangle(typeid(Derived).name()) << std::endl; }
    void on_exit(auto const& /*evt*/, auto& /*fsm*/) { std::cout << "leaving:  " << demangle(typeid(Derived).name()) << std::endl; }
};

Now all states can derive from it instead:

struct Empty : SharedBehaviours<Empty> {
    typedef mpl::vector<play> deferred_events;
};
struct Open : SharedBehaviours<Open> {
    typedef mpl::vector<play>      deferred_events;
    typedef mpl::vector1<CDLoaded> flag_list;
};

struct Stopped : SharedBehaviours<Stopped> {
    typedef mpl::vector1<CDLoaded> flag_list;
};
struct Song1 : SharedBehaviours<Song1> {
    typedef mpl::vector1<FirstSongPlaying> flag_list;
};
struct Song2 : SharedBehaviours<Song2> {};
struct Song3 : SharedBehaviours<Song3> {};

It seems to work as I expect. Making the output a bit less noisy:

template <typename Derived> struct SharedBehaviours : public msm::front::state<> {
    static std::string_view simple_name() {
        static auto const cache = [] {
            auto full = boost::core::demangle(typeid(Derived).name());
            return full.substr(full.find_last_of(":;.'\"[]{}|$") + 1);
        }();
        return cache;
    }
    void on_entry(auto const& /*evt*/, auto& /*fsm*/) { std::cout << "entering: " << simple_name() << std::endl; }
    void on_exit(auto const& /*evt*/, auto& /*fsm*/) { std::cout << "leaving:  " << simple_name() << std::endl; }
};

Prints: Live On Coliru

#include <iostream>
// back-end
#include <boost/msm/back/state_machine.hpp>
// front-end
#include <boost/msm/front/state_machine_def.hpp>

namespace msm = boost::msm;
namespace mpl = boost::mpl;

namespace {

    template <typename Derived> struct SharedBehaviours : public msm::front::state<> {
        static std::string_view simple_name() {
            static auto const cache = [] {
                auto full = boost::core::demangle(typeid(Derived).name());
                return full.substr(full.find_last_of(":;.'\"[]{}|$") + 1);
            }();
            return cache;
        }
        void on_entry(auto const& /*evt*/, auto& /*fsm*/) {
            std::cout << "entering: " << simple_name() << std::endl;
        }
        void on_exit(auto const& /*evt*/, auto& /*fsm*/) {
            std::cout << "leaving:  " << simple_name() << std::endl;
        }
    };

    // events
    struct play {};
    struct end_pause {};
    struct stop {};
    struct pause {};
    struct open_close {};
    struct NextSong {};
    struct PreviousSong {};
    struct error_found {};
    struct end_error {};
    struct end_error2 {};

    // Flags. Allow information about a property of the current state
    struct PlayingPaused {};
    struct CDLoaded {};
    struct FirstSongPlaying {};

    // A "complicated" event type that carries some data.
    struct cd_detected {
        cd_detected(std::string name) : name(name) {}

        std::string name;
    };

    // front-end: define the FSM structure
    struct player_ : public msm::front::state_machine_def<player_> {
        template <class Event, class FSM> void on_entry(Event const&, FSM&) {
            std::cout << "entering: Player" << std::endl;
        }
        template <class Event, class FSM> void on_exit(Event const&, FSM&) {
            std::cout << "leaving: Player" << std::endl;
        }

        // The list of FSM states
        struct Empty : SharedBehaviours<Empty> {
            // if the play event arrives in this state, defer it until a state
            // handles it or rejects it
            typedef mpl::vector<play> deferred_events;
        };
        struct Open : SharedBehaviours<Open> {
            // if the play event arrives in this state, defer it until a state
            // handles it or rejects it
            typedef mpl::vector<play>      deferred_events;
            typedef mpl::vector1<CDLoaded> flag_list;
        };

        struct Stopped : SharedBehaviours<Stopped> {
            // when stopped, the CD is loaded
            typedef mpl::vector1<CDLoaded> flag_list;
        };

        // the player state machine contains a state which is himself a state machine
        // as you see, no need to declare it anywhere so Playing can be developed
        // separately by another team in another module. For simplicity I just declare it
        // inside player
        struct Playing_ : public msm::front::state_machine_def<Playing_> {
            // when playing, the CD is loaded and we are in either pause or playing (duh)
            typedef mpl::vector2<PlayingPaused, CDLoaded> flag_list;

            // The list of FSM states
            struct Song1 : SharedBehaviours<Song1> {
                typedef mpl::vector1<FirstSongPlaying> flag_list;
            };
            struct Song2 : SharedBehaviours<Song2> {};
            struct Song3 : SharedBehaviours<Song3> {};
            // the initial state. Must be defined
            typedef Song1 initial_state;
            // transition actions
            void start_next_song(NextSong const&) { std::cout << "Playing::start_next_song\n"; }
            void start_prev_song(PreviousSong const&) { std::cout << "Playing::start_prev_song\n"; }
            // guard conditions

            typedef Playing_ pl; // makes transition table cleaner
            // Transition table for Playing
            // clang-format off
            struct transition_table : mpl::vector4<
                //      Start     Event         Next      Action               Guard
                //    +---------+-------------+---------+---------------------+----------------------+
                a_row < Song1   , NextSong    , Song2   , &pl::start_next_song                       >,
                a_row < Song2   , PreviousSong, Song1   , &pl::start_prev_song                       >,
                a_row < Song2   , NextSong    , Song3   , &pl::start_next_song                       >,
                a_row < Song3   , PreviousSong, Song2   , &pl::start_prev_song                       >
                //    +---------+-------------+---------+---------------------+----------------------+
            > {}; // clang-format on
            // Replaces the default no-transition response.
            template <class FSM, class Event> void no_transition(Event const& e, FSM&, int state) {
                std::cout << "no transition from state " << state << " on event " << typeid(e).name()
                          << std::endl;
            }
        };
        // back-end
        typedef msm::back::state_machine<Playing_> Playing;

        // state not defining any entry or exit
        struct Paused : SharedBehaviours<Paused> {
            typedef mpl::vector2<PlayingPaused, CDLoaded> flag_list;
        };
        struct AllOk : SharedBehaviours<AllOk> {};
        // this state is also made terminal so that all the events are blocked
        struct ErrorMode
            : // public msm::front::terminate_state<> // ErrorMode terminates the state
              // machine
              public msm::front::interrupt_state<
                  end_error /*mpl::vector<end_error,end_error2>*/> // ErroMode just
                                                                   // interrupts. Will
                                                                   // resume if the event
                                                                   // end_error is
                                                                   // generated
        {};
        // the initial state of the player SM. Must be defined
        typedef mpl::vector<Empty, AllOk> initial_state;

        // transition actions
        void start_playback(play const&) { std::cout << "player::start_playback\n"; }
        void open_drawer(open_close const&) { std::cout << "player::open_drawer\n"; }
        void close_drawer(open_close const&) { std::cout << "player::close_drawer\n"; }
        void store_cd_info(cd_detected const& cd) { std::cout << "player::store_cd_info\n"; }
        void stop_playback(stop const&) { std::cout << "player::stop_playback\n"; }
        void pause_playback(pause const&) { std::cout << "player::pause_playback\n"; }
        void resume_playback(end_pause const&) { std::cout << "player::resume_playback\n"; }
        void stop_and_open(open_close const&) { std::cout << "player::stop_and_open\n"; }
        void stopped_again(stop const&) { std::cout << "player::stopped_again\n"; }
        void report_error(error_found const&) { std::cout << "player::report_error\n"; }
        void report_end_error(end_error const&) { std::cout << "player::report_end_error\n"; }

        // guard conditions

        typedef player_ p; // makes transition table cleaner

        // Transition table for player
        // clang-format off
        struct transition_table : mpl::vector<
            //      Start     Event         Next      Action               Guard
            //    +---------+-------------+---------+---------------------+----------------------+
            a_row < Stopped , play        , Playing , &p::start_playback                         >,
            a_row < Stopped , open_close  , Open    , &p::open_drawer                            >,
            a_row < Stopped , stop        , Stopped , &p::stopped_again                          >,
            //    +---------+-------------+---------+---------------------+----------------------+
            a_row < Open    , open_close  , Empty   , &p::close_drawer                           >,
            //    +---------+-------------+---------+---------------------+----------------------+
            a_row < Empty   , open_close  , Open    , &p::open_drawer                            >,
            a_row < Empty   , cd_detected , Stopped , &p::store_cd_info                          >,
            //    +---------+-------------+---------+---------------------+----------------------+
            a_row < Playing , stop        , Stopped , &p::stop_playback                          >,
            a_row < Playing , pause       , Paused  , &p::pause_playback                         >,
            a_row < Playing , open_close  , Open    , &p::stop_and_open                          >,
            //    +---------+-------------+---------+---------------------+----------------------+
            a_row < Paused  , end_pause   , Playing , &p::resume_playback                        >,
            a_row < Paused  , stop        , Stopped , &p::stop_playback                          >,
            a_row < Paused  , open_close  , Open    , &p::stop_and_open                          >,
            //    +---------+-------------+---------+---------------------+----------------------+
            a_row < AllOk   , error_found ,ErrorMode, &p::report_error                           >,
            a_row <ErrorMode,end_error    ,AllOk    , &p::report_end_error                       >
            //    +---------+-------------+---------+---------------------+----------------------+
        > {};
        // clang-format on

        // Replaces the default no-transition response.
        template <class FSM, class Event> void no_transition(Event const& e, FSM&, int state) {
            std::cout << "no transition from state " << state << " on event " << typeid(e).name()
                      << std::endl;
        }
    };
    // Pick a back-end
    typedef msm::back::state_machine<player_> player;

    //
    // Testing utilities.
    //
    static char const* const state_names[] = {"Stopped", "Open",  "Empty",    "Playing",
                                              "Paused",  "AllOk", "ErrorMode"};

    void pstate(player const& p) {
        // we have now several active states, which we show
        for (unsigned int i = 0; i < player::nr_regions::value; ++i) {
            std::cout << " -> " << state_names[p.current_state()[i]] << std::endl;
        }
    }

    void test() {
        player p;
        // needed to start the highest-level SM. This will call on_entry and mark the
        // start of the SM
        p.start();
        // test deferred event
        // deferred in Empty and Open, will be handled only after event cd_detected
        p.process_event(play());

        // tests some flags
        std::cout << "CDLoaded active:" << std::boolalpha << p.is_flag_active<CDLoaded>()
                  << std::endl; //=> false (no CD yet)
        // go to Open, call on_exit on Empty, then action, then on_entry on Open
        p.process_event(open_close());
        pstate(p);
        p.process_event(open_close());
        pstate(p);
        p.process_event(cd_detected("louie, louie"));

        // at this point, Play is active (was deferred)
        std::cout << "PlayingPaused active:" << std::boolalpha << p.is_flag_active<PlayingPaused>()
                  << std::endl; //=> true
        std::cout << "FirstSong active:" << std::boolalpha << p.is_flag_active<FirstSongPlaying>()
                  << std::endl; //=> true

        // make transition happen inside it. Player has no idea about this event but it's
        // ok.
        p.process_event(NextSong());
        pstate(p); // 2nd song active
        p.process_event(NextSong());
        pstate(p); // 3rd song active
        p.process_event(PreviousSong());
        pstate(p); // 2nd song active
        std::cout << "FirstSong active:" << std::boolalpha << p.is_flag_active<FirstSongPlaying>()
                  << std::endl; //=> false

        std::cout << "PlayingPaused active:" << std::boolalpha << p.is_flag_active<PlayingPaused>()
                  << std::endl; //=> true
        p.process_event(pause());
        pstate(p);
        std::cout << "PlayingPaused active:" << std::boolalpha << p.is_flag_active<PlayingPaused>()
                  << std::endl; //=> true
        // go back to Playing
        // as you see, it starts back from the original state
        p.process_event(end_pause());
        pstate(p);
        p.process_event(pause());
        pstate(p);
        p.process_event(stop());
        pstate(p);
        std::cout << "PlayingPaused active:" << std::boolalpha << p.is_flag_active<PlayingPaused>()
                  << std::endl; //=> false
        std::cout << "CDLoaded active:" << std::boolalpha << p.is_flag_active<CDLoaded>()
                  << std::endl; //=> true
        // by default, the flags are OR'ed but you can also use AND. Then the flag must be
        // present in all of the active states
        std::cout << "CDLoaded active with AND:" << std::boolalpha
                  << p.is_flag_active<CDLoaded, player::Flag_AND>() << std::endl; //=> false

        // event leading to the same state
        p.process_event(stop());
        pstate(p);

        // event leading to a terminal/interrupt state
        p.process_event(error_found());
        pstate(p);
        // try generating more events
        std::cout << "Trying to generate another event"
                  << std::endl; // will not work, fsm is terminated or interrupted
        p.process_event(play());
        pstate(p);
        std::cout << "Trying to end the error" << std::endl; // will work only if ErrorMode is interrupt state
        p.process_event(end_error());
        pstate(p);
        std::cout << "Trying to generate another event"
                  << std::endl; // will work only if ErrorMode is interrupt state
        p.process_event(play());
        pstate(p);
        std::cout << "stop fsm" << std::endl;
        p.stop();
    }
} // namespace

int main() { test(); }

Output:

entering: Player
entering: Empty
entering: AllOk
CDLoaded active:false
leaving:  Empty
player::open_drawer
entering: Open
 -> Open
 -> AllOk
leaving:  Open
player::close_drawer
entering: Empty
 -> Empty
 -> AllOk
leaving:  Empty
player::store_cd_info
entering: Stopped
leaving:  Stopped
player::start_playback
entering: Song1
PlayingPaused active:true
FirstSong active:true
leaving:  Song1
Playing::start_next_song
entering: Song2
 -> Playing
 -> AllOk
leaving:  Song2
Playing::start_next_song
entering: Song3
 -> Playing
 -> AllOk
leaving:  Song3
Playing::start_prev_song
entering: Song2
 -> Playing
 -> AllOk
FirstSong active:false
PlayingPaused active:true
leaving:  Song2
player::pause_playback
entering: Paused
 -> Paused
 -> AllOk
PlayingPaused active:true
leaving:  Paused
player::resume_playback
entering: Song1
 -> Playing
 -> AllOk
leaving:  Song1
player::pause_playback
entering: Paused
 -> Paused
 -> AllOk
leaving:  Paused
player::stop_playback
entering: Stopped
 -> Stopped
 -> AllOk
PlayingPaused active:false
CDLoaded active:true
CDLoaded active with AND:false
leaving:  Stopped
player::stopped_again
entering: Stopped
 -> Stopped
 -> AllOk
leaving:  AllOk
player::report_error
 -> Stopped
 -> ErrorMode
Trying to generate another event
 -> Stopped
 -> ErrorMode
Trying to end the error
player::report_end_error
entering: AllOk
 -> Stopped
 -> AllOk
Trying to generate another event
leaving:  Stopped
player::start_playback
entering: Song1
 -> Playing
 -> AllOk
stop fsm
leaving:  Song1
leaving:  AllOk
leaving: Player

Upvotes: 1

Related Questions