sml icon indicating copy to clipboard operation
sml copied to clipboard

How to model hierarchical state machines with Boost.SML

Open ooxi opened this issue 5 years ago • 12 comments

The title says it all. Can composite state machines be used to model hierarchical ones?

ooxi avatar Jul 26 '18 07:07 ooxi

That is what I have done and it has worked fine. Be careful not to handle an event in both the parent SM and the sub SM.

cblauvelt avatar Aug 13 '18 19:08 cblauvelt

Thanks for the feedback @cblauvelt! However I don't quite get how to use composed state machines to model hierarchical ones.

I tried composing state machines as described in the examples. However, as soon as the state machine visits a state in the sub statemachine, the event will no longer be handled by the outer state machine.

Could you post an example of you you used Boost.SML to model hierarchical state machines? Even a pointer to some other project using Boost.SML in this way would be fine.

ooxi avatar Aug 13 '18 21:08 ooxi

Here is a stripped down example of how I'm using a 3 levels deep hierarchical machine.

https://gist.github.com/indiosmo/08ab24181770125d5a2448d27f6ae99f

Noticed that I did have to repeat the handler for ev_error for state<connected>. Once the machine transitions into state<connected> the error handler attached to state<protocol> at line 109 no longer responds.

indiosmo avatar Aug 14 '18 02:08 indiosmo

Thank you for the feedback @indiosmo, I'll try it myself!

ooxi avatar Aug 14 '18 10:08 ooxi

I am also interested in hierarchical state machines. I think I see how a sub-state is supposed to work, but my issue is entering a higher level state by exiting a sub-state. Please see the example UML diagram below, and the code that I have attempted to use: image

#include "../sml.hpp"
#include <iostream>

namespace sml = boost::sml;

namespace TestStateMachine
{

// events
struct Event1 {};
struct Event2 {};
struct Event3 {};
struct Event4 {};

// states
class State1;
class State2;
class State3;
class State4;
class State5;

auto state5 = sml::state<State5>;

struct BaseState
{
    auto operator()() const noexcept {
        using namespace sml;
        return make_transition_table(
            *state<State2> + on_entry<_> / []{ std::cout << "Entering State2" << std::endl; },
             state<State2> + event<Event2> = state<State4>,
             state<State4> + on_entry<_> / []{ std::cout << "Entering State4" << std::endl; },
             state<State4> + event<Event3> = state5
        );
    }
};


struct StateMachine
{
    auto operator()() const noexcept {
        using namespace sml;
        return make_transition_table(
            *state<State1> + on_entry<_> / []{ std::cout << "Entering State1" << std::endl; },
            state<State1> + event<Event1> = state<BaseState>,
            state<BaseState> + on_entry<_> / []{ std::cout << "Entering BaseState" << std::endl; },
            state<BaseState> + event<Event4> = state<State3>,
            state<State3> + on_entry<_> / []{ std::cout << "Entering State3" << std::endl; },
            state5 + on_entry<_> / []{ std::cout << "Entering State5" << std::endl; }
        );
    }
};
}

#include <cassert>

int main()
{
    using namespace sml;
    using namespace TestStateMachine;

    sm<StateMachine> sm{};
    assert(sm.is(state<State1>));

    sm.process_event(Event1{});
    assert(sm.is(state<BaseState>));
    assert(sm.is<decltype(state<BaseState>)>(state<State2>));

    sm.process_event(Event2{});
    assert(sm.is(state<BaseState>));
    assert(sm.is<decltype(state<BaseState>)>(state<State4>));

    sm.process_event(Event3{});
    assert(sm.is(TestStateMachine::state5));  // <-- Assert fails because in BaseState/State5, expecting State5
}

I'm expecting the top level state machine to change, but that obviously doesn't happen. How can I get the message from the BaseState machine to the higher level StateMachine that the BaseState is complete and BaseState::State5 should be transitioned to? Also, since I couldn't figure out how to transition directly to State2, I'm using it as the initial target.

jovere avatar Oct 01 '18 20:10 jovere

Hi! Although i have just started using sml, i believe that a direct transition from a sub-state to an external state is not possible without having transitioned to the contained BaseState. I was stuck with a similar issue a while ago. Try implementing the following approach, use an auxiliary event which triggers a self-transition, this in turn causes "exit" of the sub-state and allows us to handle the external transition from base-state to state5 via process_event() in an action.

The following code works as intended. cheers!

#include "boost/sml.hpp"
#include <iostream>

namespace sml = boost::sml;

namespace TestStateMachine
{

// events
struct Event1 {};
struct Event2 {};
struct Event3 {};
struct Event4 {};
struct EventExt {}; //added an auxiliary internal event

// states
class State1;
class State2;
class State3;
class State4;
class State5;

auto state5 = sml::state<State5>;

struct BaseState
{
    auto operator()() const noexcept {
        using namespace sml;
        return make_transition_table(
            *state<State2> + on_entry<_> / []{ std::cout << "Entering State2" << std::endl; },
             state<State2> + event<Event2> = state<State4>,
             state<State4> + on_entry<_> / []{ std::cout << "Entering State4" << std::endl; },
             //This i believe propagates Event3 to the contained state.
             state<State4> + event<EventExt>/process(Event3{}) = state<State4>
        );
    }
};


struct StateMachine
{
    auto operator()() const noexcept {
        using namespace sml;
        return make_transition_table(
            *state<State1> + on_entry<_> / []{ std::cout << "Entering State1" << std::endl; },
            state<State1> + event<Event1> = state<BaseState>,
            state<BaseState> + on_entry<_> / []{ std::cout << "Entering BaseState" << std::endl; },
            state<BaseState> + event<Event4> = state<State3>,
	    state<BaseState> + event<Event3> = state5,
            state<State3> + on_entry<_> / []{ std::cout << "Entering State3" << std::endl; },
            state5 + on_entry<_> / []{ std::cout << "Entering State5" << std::endl; }
        );
    }
};
}

#include <cassert>

int main()
{
    using namespace sml;
    using namespace TestStateMachine;

    sm<StateMachine> sm{};
    assert(sm.is(state<State1>));

    sm.process_event(Event1{});
    assert(sm.is(state<BaseState>));
    assert(sm.is<decltype(state<BaseState>)>(state<State2>));

    sm.process_event(Event2{});
    assert(sm.is(state<BaseState>));
    assert(sm.is<decltype(state<BaseState>)>(state<State4>));

    sm.process_event(Event3{});
    assert(sm.is(TestStateMachine::state5));  // <-- Assert fails because in BaseState/State5, expecting State5
}

Tal-seven avatar Oct 05 '18 16:10 Tal-seven

@Tal-seven I don't think the line marked "This i believe propagates Event3 to the contained state" matters here. It's the line state<BaseState> + event<Event3> = state5, that moves the base state machine to state5. This can be seen by commenting the first and getting the same results.

And the line state<BaseState> + event<Event3> = state5 also allows transitioning from State2 to State5, which doesn't fit the UML.

We need to use the last line, but with a guard according to whether or not BaseState is in State4. I didn't see this implemented already, so I copy pasted (and slightly modified) the is member of the type sm. The usage is the line state<BaseState> + event<Event3> [isin(state<BaseState>, state<State4>)] = state5 in StateMachine.

#include "boost/sml.hpp"
#include <iostream>

namespace boost {
namespace sml {
namespace front {
namespace actions{
struct isin{
  template <class sm, class TState>
  class isin_impl;
  template <class SubTSM, class TState>
  class isin_impl<back::sm<SubTSM>, TState> : public front::action_base {
   public:
    explicit isin_impl(const TState &state) : state(state) {}
    template <class T, class TSM, class TDeps, class TSubs>
    bool operator()(const T &, TSM &sm, TDeps &, TSubs & subs) {
      using sm_impl_t = back::sm_impl<SubTSM>;
      using state_t = typename sm_impl_t::state_t;
      using states_ids_t = typename sm_impl_t::states_ids_t;
      return aux::get_id<state_t, typename TState::type>((states_ids_t *)0) == aux::cget<sm_impl_t>(subs).current_state_[0];
    }

   private:
    TState state;
  };
  template <class TSubTSM, class TState>
  auto operator()(const TSubTSM&, const TState &state) {
    using SubTSM = typename TSubTSM::type;
    return isin_impl<SubTSM, TState>{state};
  }
};
} // namespace actions
} // namespace front
static front::actions::isin isin;
} // namespace sml
} //namespace boost

namespace sml = boost::sml;

namespace TestStateMachine
{

// events
struct Event1 {};
struct Event2 {};
struct Event3 {};
struct Event4 {};

// states
class State1;
class State2;
class State3;
class State4;
class State5;

auto state5 = sml::state<State5>;

struct BaseState
{
    auto operator()() const noexcept {
        using namespace sml;
        return make_transition_table(
            *state<State2> + on_entry<_> / []{ std::cout << "Entering State2" << std::endl; },
             state<State2> + event<Event2> = state<State4>,
             state<State4> + on_entry<_> / []{ std::cout << "Entering State4" << std::endl; }
        );
    }
};


struct StateMachine
{
    auto operator()() const noexcept {
        using namespace sml;
        return make_transition_table(
            *state<State1> + on_entry<_> / []{ std::cout << "Entering State1" << std::endl; },
            state<State1> + event<Event1> = state<BaseState>,
            state<BaseState> + on_entry<_> / []{ std::cout << "Entering BaseState" << std::endl; },
            state<BaseState> + event<Event4> = state<State3>,
	    state<BaseState> + event<Event3> [isin(state<BaseState>, state<State4>)] = state5,
            state<State3> + on_entry<_> / []{ std::cout << "Entering State3" << std::endl; },
            state5 + on_entry<_> / []{ std::cout << "Entering State5" << std::endl; }
        );
    }
};
}

#include <cassert>

int main()
{
    using namespace sml;
    using namespace TestStateMachine;

    sm<StateMachine> sm{};
    assert(sm.is(state<State1>));

    sm.process_event(Event1{});
    assert(sm.is(state<BaseState>));
    assert(sm.is<decltype(state<BaseState>)>(state<State2>));

    sm.process_event(Event2{});
    assert(sm.is(state<BaseState>));
    assert(sm.is<decltype(state<BaseState>)>(state<State4>));

    sm.process_event(Event3{});
    assert(sm.is(TestStateMachine::state5));
}

I'm sure it's possible to remove the repetition of the symbol state<BaseState>, but I didn't think about this much. Also, having the ability to send the submachine in which to look allows for using isin in a different parallel state.

drorspei avatar Dec 23 '18 02:12 drorspei

@drorspei thank you for the create example. It would be nice to have this function build into the library. Are you willing to create a pull request?

There are some limitations to the guard:

  • It is not possible to leave a sub state just with a guard (without event)
  • The guard fails to compile if deferred events are enabled

erikzenkerLogmein avatar Mar 15 '19 08:03 erikzenkerLogmein

@erikzenkerLogmein You're welcome! I won't be creating a pull request for a few reasons. First, I'm not sure if checking for states is in the UML State Diagrams spirit, or even specification. I've tried going over the latest OMG specification and couldn't find anything related. Second, it's not clear what the semantics of these guards should be, like where they can be used, order of evaluation, etc.. For example, it's not clear to me that we should be able to leave a substate with a guard, as you suggest. As far as I understand UML State Diagrams, exiting of substates is supposed to be done from a unique exit state, but these aren't currently defined in SML. Third, I have no idea how to implement checking within a submachine where we are in the parent machine, though this feature might be less important.

In regards to the guard failing when deferred events are enabled: yes, that's a bug. I didn't propagate policies into the template magic. Here's an update that makes the example work again, and with some compiler explorer debugging I think I now correctly pass all policies, so this should also work regardless of logger, thread safety, and process queue policies either. But honestly this level of meta programming is quite above my head and the code below could be total rubbish :)

namespace boost {
namespace sml {
namespace front {
namespace actions{
struct isin{
  template <class sm, class TState>
  class isin_impl;
  template <class SubTSM, class TState>
  class isin_impl<back::sm<SubTSM>, TState> : public front::action_base {
   public:
    explicit isin_impl(const TState &state) : state(state) {}
    template <class T, class TSM, class TDeps, class TSubs>
    bool operator()(const T &, TSM &sm, TDeps &, TSubs & subs) {
      using sm_impl_w_policies_t = typename SubTSM::template rebind<TSM>::sm;
      using sm_impl_t = back::sm_impl<SubTSM>;
      using state_t = typename sm_impl_t::state_t;
      using states_ids_t = typename sm_impl_t::states_ids_t;
      return aux::get_id<state_t, typename TState::type>((states_ids_t *)0) == aux::cget<sm_impl_w_policies_t>(subs).current_state_[0];
    }

   private:
    TState state;
  };
  template <class TSubTSM, class TState>
  auto operator()(const TSubTSM&, const TState &state) {
    using SubTSM = typename TSubTSM::type;
    return isin_impl<SubTSM, TState>{state};
  }
};
} // namespace actions
} // namespace front
static front::actions::isin isin;
} // namespace sml
} //namespace boost

drorspei avatar Mar 15 '19 17:03 drorspei

@drorspei Thank you for pointing that out.

Tal-seven avatar Apr 22 '19 08:04 Tal-seven

@drorspei Thanks for your workaround. The first version appears to work fine, but the second one for some reason always returns false...

I'm also not quire following the magic here: What is the reason for the TState state member?

Rijom avatar Jan 14 '20 10:01 Rijom

Because of missing hierarchical features in sml I started to to work on a state machine library with better hierarchical support. Check it out https://github.com/erikzenker/hsm.

erikzenker avatar May 23 '20 11:05 erikzenker