Reputation: 355
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
Reputation: 3550
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.
// 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;
};
The variables cleared
and close_requested
are encoded as member variables.
These variables are referenced in guards and updated in actions:
e_close
in the "opened" state, the guard checks the cleared
variable.
cleared
is true, the state transitions to "closed."e_leave
in the "opened" state, the guard checks the close_requested
variable.
close_requested
is true, the state transitions to "closed."Additional information can be accommodated in the future by introducing new member variables.
Upvotes: 0
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:
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