Nicolas
Nicolas

Reputation: 355

State machine best practice for requiring two events for transition (boost::ext SML)

I'm trying to implement a state machine using SML and I have a question regarding the best practice for a particular scenario.

Let's say the state machine represents a gate that can open and close on user input but only closes when it's safe to do so (the user is not near the gate).

So in order to close the gate, I need to have both a user command to close and that the user is clear of the gate.

The latter (gate clear) seems like an event to me but then I have the issue that I'm trying to implement a state transition from open to closing requiring two events (close and userClear). I don't know in which order these two events will come, userClear may even happen before entering the state open, or never fire because the user was never too close to the gate.

In the SML examples, I see that it's possible to defer events. Maybe that's useful somehow?

I could also encode the clear status into the state. This would multiply the number of states I have, leading to a transition table like

openNotClear        + clear = openClear,
openClear           + close = closingClear,
openNotClear        + close = openWaitingForClear,
openWaitingForClear + close = closingClear,
...

This doesn't seem ideal since the state space can get quite large quite quickly.

I'm sure this kind of situation is not uncommon and there is a standard approach to dealing with such multiple events/conditions.

How is this best dealt with in an event-driven state machine?

Upvotes: 0

Views: 69

Answers (2)

Takatoshi Kondo
Takatoshi Kondo

Reputation: 3550

Strategy

Utilize orthogonal state functionality. As far as I know, SML does not provide a built-in mechanism to determine the current state of a counterpart orthogonal state.

To achieve this, introduce member variables and update them accordingly. These member variables should represent the current state of the counterpart orthogonal state.

Once implemented, use these member variables in your guard conditions.

This approach avoids an explosion of state combinations.

Code

// Events
struct e_open{};
struct e_close{};
struct e_arrive{};
struct e_leave{};

class mysm {
    using Self = mysm;
public:
    auto operator()() {
        using namespace sml;
        // Guards
        auto g_close_requested     = [this] { return close_requested; };
        auto g_cleared             = [this] { return cleared; };
        // Actions
        auto a_arrive              = [this] { cleared = false; };
        auto a_leave               = [this] { cleared = true; };
        auto a_close_request       = [this] { close_requested = true; };
        auto a_clear_close_request = [this] { close_requested = false; };

        return make_transition_table(
            // Orthogonal state 1 (cleared/not_cleared)
            * "cleared"_s     + event<e_arrive>                                             = "not_cleared"_s
            , "cleared"_s     + sml::on_entry<_>                    / a_leave
            , "not_cleared"_s + event<e_leave>                                              = "cleaered"_s
            , "not_cleared"_s + sml::on_entry<_>                    / a_arrive
            ,
            // Orthogonal state 2 (opneded/closed)
            * "closed"_s      + event<e_open>                                               = "opened"_s
            , "opened"_s      + event<e_close> [ g_cleared]                                 = "closed"_s
            , "opened"_s      + event<e_close> [!g_cleared]         / a_close_request
            , "opened"_s      + event<e_leave> [ g_close_requested] / a_clear_close_request = "closed"_s
        );
    }

private:
    bool cleared = true;
    bool close_requested = false;
};

Demo (with log)

View on Godbolt

Details

The variables cleared and close_requested are encoded as member variables.
These variables are referenced in guards and updated in actions:

  • When processing e_close in the "opened" state, the guard checks the cleared variable.
    • If cleared is true, the state transitions to "closed."
  • When processing e_leave in the "opened" state, the guard checks the close_requested variable.
    • If close_requested is true, the state transitions to "closed."

Additional information can be accommodated in the future by introducing new member variables.

Upvotes: 0

BNilsou
BNilsou

Reputation: 895

If the framework you're using allows it, would suggest to use parallel states.

Also, in this answer I will take into account the fact that when your close event is fired, then the gate should close whenever possible without any other close event required. Because of that, there is no choice than having more than two states for the door:

Gate State Machine
Closed         + open      = Opened
Opened         + close     = AboutToClose
AboutToClose   + userClear = Closed

Now, let's imagine some parallel states (or a second state machine, running in parallel) representing the business around the gate, depending on the player proximity:

Proximity State Machine
Clear  + userComing   = Busy
Busy   + userLeaving  = Clear

Now, you would need to add two additional checks to have the final implementation:

  1. When entering the state 'AboutToClose' of the Gate state machine, verify the state of the Proximity state machine. If it is 'Clear', then generate a 'userClear' event to then transition to the 'Closed' state.
  2. When entering the state 'Clear' of the Proximity state machine, generate a 'userClear' event.

This would look something like this:

func1: onAboutToCloseEntered:
  if (proximity.state == Clear)
    generateEvent(userClear)

func2: onClearEntered:
  generateEvent(userClear)

If the state of the Proximity state machine is Busy when entering the 'AboutToClose' state of the Gate state machine, then just do nothing (func1). The Gate state machine will stay in the AboutToClose state. Later on, when the user will move away from the gate, the event userClear will be generated (func2).

If the gate needs to be open again while being in the AboutToClose state, then just add a new transition to the Opened state.

Upvotes: 0

Related Questions