.. currentmodule:: edzed ===================== Finite-State Machines ===================== A Finite-State Machine (**FSM**) is a special kind of sequential blocks. Due to its versatility and also its complexity it deserves a separate chapter. *A basic understanding of the Finite-State Machine concept is helpful.* With some simplification, a Finite-State Machine is defined by: - set of possible states, one of them being the initial state - set of events it can process; events trigger transitions from one state to another - a control table called a transition table An FSM block implements several additional features: - persistent state - timed events - event conditions - entry and exit actions - generated events .. note:: Every sequential block (SBlock) has its internal state and can process events. An FSM also defines states and events. Their relationship is: - FSM state is a part of SBlock's internal state. - all events are delivered using the :meth:`SBlock.event` method as in any other sequential block. Usually all events are processed by the FSM code, i.e. they are FSM events, but please note that it is possible to add non-FSM events to be processed by the underlying SBlock and bypassing the FSM. See :meth:`SBlock._event_ETYPE`. In this chapter by 'state' and 'event' we usually mean an FSM state and an FSM event respectively. FSM example =========== Let's start with a simple example:: class Turnstile(edzed.FSM): STATES = ['locked', 'unlocked'] EVENTS = [ ['coin', ['locked'], 'unlocked'], ['push', ['unlocked'], 'locked'], ] t1 = Turnstile('t1', comment="example turnstile #1") t2 = Turnstile('t2', comment="example turnstile #2") We have defined a new FSM and created two circuit blocks. The :class:`Turnstile` has two states: 'locked' and 'unlocked', the first one is the initial state by default. It accepts two events: 'coin' and 'push'. When the event 'coin' occurs in the 'locked' state, the FSM makes a transition to the 'unlocked' state. When the event 'push' occurs in the 'unlocked' state, the FSM makes a transition to the 'locked' state. There are no other state transitions defined. For example, when the turnstile is 'unlocked', the 'coin' event will have no effect. We may say that an event is not accepted, not allowed, or even rejected in certain state, but it's a normal FSM operation - not an error. Creating FSMs types =================== A new FSM is created by subclassing the base class. Instances of the subclass will be circuit blocks. .. class:: FSM Base class for creating FSMs. Subclasses are supposed to define these class attributes: - :obj:`FSM.STATES` - :obj:`FSM.EVENTS` - :obj:`FSM.TIMERS` All three are empty by default. A subclass may also define: - :ref:`event conditions` - :ref:`state entry and exit actions` States, events, transitions --------------------------- An FSM has a current state. A transition from the current state to the next state is triggered by a received event. The next state is determined by a transition table lookup: (current state, event) --> next state All states and regular events are represented by a name (string). Avoid any special characters in names, because function names are derived from them. States and events form two separate namespaces, but using the same name for both is discouraged. The :meth:`FSM.event` method returns ``True`` for accepted FSM events and ``False`` for rejected FSM events. .. attribute:: FSM.STATES :type: Sequence[str] Class attribute. A sequence of valid states. Timed states from :obj:`FSM.TIMERS` are appended automatically, but may be listed here, because duplicates do not matter. The very first item in the resulting list is the default initial state. .. attribute:: FSM.EVENTS :type: Sequence[Sequence] Class attribute. The transition table as a sequence of transition rules. Each rule in the sequence has three items:: (event: str, states: str|Sequence[str]|None, next_state: str|None) *states* (item 2) define in which states will the *event* (item 1) trigger a transition to the *next_state* (item 3). The order of rules does not matter, but the transition table must be deterministic. Only one next state may be defined for any combination of event and state. Data format of a table entry in detail: - *event* is always a string, the name of an event. Only events found in the ``EVENTS`` table are valid events for the given FSM. - *states* must be one of: - a string containing one or more states. Multiple states must be separated by ``'|'`` (vertical bar); whitespace may be added for readability. - a sequence of states (strings) - ``None`` as a special value for any state. An entry with ``None`` has lower precedence than an entry with explicitly listed states. .. versionadded:: 23.2.14 Added the "union" syntax: ``'state1 | state2 | ...'`` - *next_state* must be: - a single state (string), or - ``None`` to make a transition explicitly disallowed. Examples of ``EVENTS`` entries:: #1A ('start', ['on', 'off'], 'on'), #1B - same as 1A, alternative notation ('start', 'on | off', 'on'), #2 - same as above if there exist only the 'on' and 'off states ('start', None, 'on'), #3A ('push', ['unlocked'], 'locked'), #3B - same as 3A, alternative notation ('push', 'unlocked', 'locked'), #4 ["ev1", None, "state2"], # default rule for "ev1" and all states except # more specific rules for state2 and state3 below ["ev1", ["state2"], "state3"], # rule for state2 -> state3 ["ev1", ["state3"], None], # ev1 is ignored in state3 .. attribute:: FSM.TIMERS :type: Mapping[str, Sequence] Class attribute. Specification of optional timers attached to selected states. A state with a timer is called "timed state". Apart from the timer are timed states not different from other states and they automatically belong to the list of states :obj:`FSM.STATES`. Data format: dictionary of ``{timed_state: (default_duration, timed_event)}`` A timer is set when the *timed_state* is entered. When the timer expires, the *timed_event* is generated. If the state is exited before the timer expiration, the timer is cancelled. This means that a transition from a timed state to the same state restarts the timer. If this is undesirable, disallow the transition. If the *timed_event* gets rejected, the block stays in *timed_state* without a timer. See also: :ref:`Goto special event`. If the duration is 0.0, the *timed_event* is generated immediately. If the duration is :const:`INF_TIME` (infinite time to expiration), the timer won't be set at all. Instances can modify the default duration with ``t_STATE=value`` keyword argument. The duration can be dynamically overridden with a ``'duration': value`` data item passed with the event responsible for entering the timed state. This value has the highest precedence. The timer duration may be given as: - number of seconds (int, float), negative values are replaced with 0.0 - a :ref:`string with time units