fpp/docs/users-guide/Defining-State-Machines.adoc
2025-01-22 18:21:57 -08:00

1770 lines
53 KiB
Plaintext

== Defining State Machines
A *hierarchical state machine* (state machine for short)
is a software subsystem that specifies the following:
* A set of *states* that the system can be in.
The states can be arranged in a hierarchy (i.e.,
states may have substates).
* A set of *transitions* from one state to another that
occur under specified conditions.
State machines are important in embedded programming.
For example, F Prime components often have a concept of state
that changes as the system runs, and it is useful to model
these state changes as a state machine.
In FPP there are two ways to define a state machine:
. An *external* state machine definition is like
an <<Defining-Types_Abstract-Type-Definitions,abstract type definition>>:
it tells the analyzer that a state machine exists with a specified
name, but it says nothing about the state machine behavior.
An external tool must provide the state machine implementation.
. An *internal* state machine definition is like an
<<Defining-Types_Array-Type-Definitions,array type definition>>
or
<<Defining-Types_Struct-Type-Definitions,struct type definition>>:
it provides a complete specification in FPP of the state machine behavior.
The FPP back end uses this specification to generate code;
no external tool is required.
The following subsections describe both kinds of state machine
definitions.
State machine definitions may appear at the top level or inside a
<<Defining-Modules,module definition>>.
A state machine definition is an
<<Writing-Comments-and-Annotations_Annotations,annotatable element>>.
Once you define a state machine, you can
<<Defining-Components_State-Machine-Instances,
instantiate it as part of a component>>.
The component can then send the signals that cause the state
transitions.
The component also provides the functions that are called when
the state machine does actions and evaluates guards.
=== Writing a State Machine Definition
*External state machines:*
To define an external state machine, you write the keywords
`state` `machine` followed by an identifier, which is the
name of the state machine:
[source,fpp]
----
state machine M
----
This code defines an external state machine with name `M`.
When you define an external state machine `M`, you must provide
an implementation for `M`, as discussed in the section
on <<Writing-C-Plus-Plus-Implementations_Implementing-External-State-Machines,
implementing external state machines>>.
The external implementation must have a header file `M.hpp`
located in the same directory as the FPP file where
the state machine `M` is defined.
*Internal state machines:*
In the following subsections, we explain how to define internal
state machines in FPP.
The behavior of these state machines closely follows the behavior
described for state machines in the
https://www.omg.org/spec/UML/2.5.1/PDF[Universal Modeling Language (UML)].
UML is a graphical language and FPP is a textual language,
but each of the concepts in the FPP language is motivated
by a corresponding UML concept.
FPP does not represent every aspect of UML state machines:
because our goal is to support embedded and flight software,
we focus on a subset of UML state machine behavior that is
(1) simple and unambiguous and (2) sufficient for embedded
applications that use F Prime.
In this manual, we focus on the syntax and high-level behavior
of FPP state machines.
For more details about the {cpp} code generation
for state machines, see the
https://fprime.jpl.nasa.gov/latest/docs/user-manual/design/state-machines/[F
Prime design documentation].
=== States, Signals, and Transitions
In this and the following sections, we explain how to define
internal state machines, i.e., state machines that are fully
specified in FPP.
In these sections, when we say "`state machine,`"
we mean an internal state machine.
First we explain the concepts of states,
signals, and transitions.
These are the basic building blocks of FPP state machines.
*Basic state machines:*
The simplest state machine _M_ that has any useful
behavior consists of the following:
* Several *state definitions*.
These define the states of _M_.
* An *initial transition specifier*.
This specifies the state that an instance of _M_ is in when it starts up.
* One or more *signal definitions*.
The external environment (typically an F Prime component)
sends signals to instances of _M_.
* One or more *state transition specifiers*.
These tell each instance of _M_ what to do when it receives a signal.
Here is an example:
[source,fpp]
----
@ A state machine representing a device with on-off behavior
state machine Device {
@ A signal for turning the device on
signal cmdOn
@ A signal for turning the device off
signal cmdOff
@ The initial state is OFF
initial enter OFF
@ The ON state
state ON {
@ In the ON state, a cmdOff signal causes a transition to the OFF state
on cmdOff enter OFF
}
@ The OFF state
state OFF {
@ In the OFF state, a cmdOff signal causes a transition to the ON state
on cmdOn enter ON
}
}
----
Here is the example represented graphically, as a UML state
machine:
image::users-guide/diagrams/state-machine/Basic.png[Device state machine,200,align="center"]
This example defines a state machine `Device` that represents
a device with on-off behavior.
There are two states, `ON` and `OFF`.
The initial transition specifier `initial enter OFF`
says that the state machine is in state `OFF` when it starts up.
There are two signals: `cmdOn` for turning the device
on and `cmdOff` for turning the device off.
There are two state transition specifiers:
* A specifier that says to enter state `OFF`
on receiving the `cmdOff` signal in state `ON`.
* A specifier that says to enter state `ON`
on receiving the `cmdOn` signal in state `OFF`.
In general, a state transition specifier causes a transition
from the state _S_ in which it appears to the state _S'_ that
appears after the keyword `enter`.
We say that _S_ is the *source* of the transition,
and _S'_ is the *target* of the transition.
If a state machine instance receives a signal _s_, and it is
in a state _S_ that specifies no behavior for signal _s_
(i.e., there is no transition with source _S_ and signal _s_),
then nothing happens.
In the example above, if the state machine receives signal
`cmdOn` in state `ON`
or signal `cmdOff` in state `OFF`, then it takes no action.
*Rules for defining state machines:*
Each state machine definition must conform to the following rules:
. A state machine definition and each of its members is an
<<Writing-Comments-and-Annotations_Annotations,annotatable element>>.
For example, you can annotate the `Device` state machine as shown above.
The members of a state machine definition form an
<<Defining-Constants_Multiple-Definitions-and-Element-Sequences,
element sequence>> with a semicolon as the optional
terminating punctuation.
The same rules apply to the members of a state definition.
. Each state machine definition must have exactly one
initial transition specifier that names a state of
the state machine.
For example, if we deleted the initial transition specifier
from the example above and passed the result through
`fpp-check`, an error would occur.
. Every state definition must be reachable from the initial
transition specifier or from a state transition specifier.
For example, if we deleted the state transition specifier
in the state `ON` from the example above and passed
the result through `fpp-check`, an error would occur.
In this case, there would be no way to reach the `ON` state.
. Every state name must be unique, and every signal name
must be unique.
. Each state may have at most one state transition specifier
for each signal _s_.
For example, if we added another transition to state `ON`
on signal `cmdOff`, the FPP analyzer would report an error.
*Simple state definitions:*
If a state definition has no transitions, then you can omit
the braces.
For example, here is a revised version of the `Device` state
machine that has an off-on transition but no on-off transition:
[source,fpp]
----
@ A state machine representing a device with on-only behavior
state machine Device {
@ A signal for turning the device on
signal cmdOn
@ The initial state is OFF
initial enter OFF
@ The ON state
state ON
@ The OFF state
state OFF {
@ In the OFF state, a cmdOff signal causes a transition to the ON state
on cmdOn enter ON
}
}
----
Notice that state `ON` has a simple definition with no curly braces.
=== Actions
An *action* is a function that a state machine calls at a
specified point in its behavior.
In the FPP model, actions are abstract;
in the {cpp} back end they become pure virtual functions that
you implement.
When a state machine instance calls the function associated with
an action _A_, we say that it *does* _A_.
==== Actions in Transitions
To define an action, you write the keyword `action` followed
by the name of the action.
As with <<Defining-State-Machines_States-Signals-and-Transitions,signals>>,
every action name must be unique.
To do an action, you write the keyword `do`
followed by a list of action names enclosed in curly braces.
You can do this in an initial transition specifier or in a
state transition specifier.
As an example, here is the `Device` state machine from the
previous section, with actions added:
[source,fpp]
----
@ A state machine representing a device with on-off behavior,
@ with actions on transitions
state machine Device {
@ Initial action 1
action initialAction1
@ Initial action 2
action initialAction2
@ An action on the transition from OFF to ON
action offOnAction
@ An action on the transition from ON to OFF
action onOffAction
@ A signal for turning the device on
signal cmdOn
@ A signal for turning the device off
signal cmdOff
@ The initial state is OFF
@ Before entering the initial state, do initialAction1 and then initialAction2
initial do { initialAction1, initialAction2 } enter OFF
@ The ON state
state ON {
@ In the ON state, a cmdOff signal causes a transition to the OFF state
@ Before entering the OFF state, do onOffAction
on cmdOff do { onOffAction } enter OFF
}
@ The OFF state
state OFF {
@ In the OFF state, a cmdOff signal causes a transition to the ON state
@ Before entering the ON state, do offOnAction
on cmdOn do { offOnAction } enter ON
}
}
----
Here is the graphical representation:
image::users-guide/diagrams/state-machine/ActionsInTransitions.png[Device state machine with actions on transitions,500,align="center"]
In this example there are four actions:
`initialAction1`, `initialAction2`, `offOnAction`, and `onOffAction`.
The behavior of each of these actions is specified in the {cpp}
implementation; for example, each could emit an
<<Defining-Components_Events,F Prime event>>.
The state machine has the following behavior:
* On startup, do `initialAction1`, do `initialAction2`, and
enter the `OFF` state.
* In state `OFF`, on receiving the `cmdOn` signal, do
`offOnAction` and enter the `ON` state.
* In state `ON`, on receiving the `cmdOff` signal, do
`onOffAction` and enter the `OFF` state.
When multiple actions appear in an action list, as in the initial
transition specifier shown above, the actions occur in the order
listed.
Each action list is an
<<Defining-Constants_Multiple-Definitions-and-Element-Sequences,
element sequence>> with a comma as the optional
terminating punctuation.
==== Entry and Exit Actions
In addition to doing actions on transitions, a state machine
can do actions on entry to or exit from a state.
To do actions like this, you write *state entry specifiers*
and *state exit specifiers*.
For example, here is the `Device` state machine from
the previous section, with state entry and exit specifiers
added to the `ON` and `OFF` states:
[source,fpp]
----
@ A state machine representing a device with on-off behavior,
@ with actions on transitions and on state entry and exit
state machine Device {
@ Initial action 1
action initialAction1
@ Initial action 2
action initialAction2
@ An action on the transition from OFF to ON
action offOnAction
@ An action on the transition from ON to OFF
action onOffAction
@ An action on entering the ON state
action enterOn
@ An action on exiting the ON state
action exitOn
@ An action on entering the OFF state
action enterOff
@ An action on exiting the OFF state
action exitOff
@ A signal for turning the device on
signal cmdOn
@ A signal for turning the device off
signal cmdOff
@ The initial state is OFF
@ Before entering the initial state, do initialAction1 and then initialAction2
initial do { initialAction1, initialAction2 } enter OFF
@ The ON state
state ON {
@ On entering the ON state, do enterOn
entry do { enterOn }
@ In the ON state, a cmdOff signal causes a transition to the OFF state
@ Before entering the OFF state, do offAction
on cmdOff do { onOffAction } enter OFF
@ On exiting the ON state, do exitOn
exit do { exitOn }
}
@ The OFF state
state OFF {
@ On entering the OFF state, do enterOff
entry do { enterOff }
@ In the OFF state, a cmdOff signal causes a transition to the ON state
@ Before entering the ON state, do onAction
on cmdOn do { offOnAction } enter ON
@ On exiting the OFF state, do exitOff
exit do { exitOff }
}
}
----
Here is the graphical representation:
image::users-guide/diagrams/state-machine/EntryAndExitActions.png[Device state machine with entry and exit actions,500,align="center"]
As with actions on transitions, each entry or exit specifier names
a list of actions, and the actions are done in the order named.
The entry actions are done just before entering the state,
and the exit actions are done just before exiting the state.
For example, if the state machine is in state `OFF` and it
receives a `cmdOn` signal, then it runs the following behavior,
in the following order:
* Exit state `OFF`. On exit, do `exitOff`.
* Transition from `OFF` to `ON`. On the transition, do `offOnAction`.
* Enter state `ON`. On entry, do `enterOn`.
Each state may have at most one entry specifier and at most one
exit specifier.
==== Typed Signals and Actions
Optionally, signals and actions may carry data values.
To specify that a signal or action carries a data value,
you write a colon and a data type at the end of the
signal or action specifier.
For example, here is a `Device` state machine in which
the `cmdOn` signal and the `offOnAction` each carries
a `U32` counter value:
[source,fpp]
----
@ A state machine representing a device with on-off behavior,
@ with actions on transitions
state machine Device {
@ An action on the transition from OFF to ON
@ The value counts the number of times this action has occurred
action offOnAction: U32
@ A signal for turning the device on
@ The value counts the number of times this signal has been received
signal cmdOn: U32
@ A signal for turning the device off
signal cmdOff
@ The initial state is OFF
initial enter OFF
@ The ON state
state ON {
@ In the ON state, a cmdOff signal causes a transition to the OFF state
on cmdOff enter OFF
}
@ The OFF state
state OFF {
@ In the OFF state, a cmdOff signal causes a transition to the ON state
@ Before entering the ON state, do offOnAction, passing the data from
@ the signal into the action
on cmdOn do { offOnAction } enter ON
}
}
----
When you send the `cmdOn` signal to an instance of
this state machine, you must provide a `U32` value.
When the state machine is in the `OFF` state and it receives
this signal, it does action `offOnAction` as shown.
The function that defines the behavior of `offOnAction` has
a single argument of type `U32`.
The value provided when the signal is sent is passed as the
argument to this function.
Here are the rules for writing typed signals and actions:
* When you do an action that has a type, a value
compatible with that type must be available.
For example, we can't do the `offOnAction` in the `cmdOff` transition shown
above, because no `U32`
value is available there.
Similarly, no action done in a
<<Defining-State-Machines_Actions_Entry-and-Exit-Actions,
state entry or exit specifier>> may carry a value,
because no values are available on entry to or exit
from a state.
* The type that appears in a signal or an action can
be any FPP type.
In the example above we used a simple `U32` type;
we could have used, for example, a struct or array type.
In particular, you can use a struct type to send several
data values, with each value represented as a member
of the struct.
* When doing an action with a value, you don't have to
make the types exactly match.
For example, you are permitted to pass a `U16` value
to an action that requires a `U32` value.
However, the type of the value must be convertible to the type
specified in the action.
The type conversion rules are spelled out in full in
_The FPP Language Specification_.
In general, the analyzer will allow a conversion if it
can be safely done for all values of the original type.
* If an action _A_ does not carry any value, then you
can do _A_ in any context, even if a value is available there.
For example, in the code shown above, the `cmdOn`
transition could do some other action that carries no value.
In this case the value is ignored when doing the action.
=== More on State Transitions
In this section, we provide more details on how to write
<<Defining-State-Machines_States-Signals-and-Transitions,state transitions>>
in FPP state machines.
==== Guarded Transitions
Sometimes it is useful to specify that a transition should occur
only if a certain condition is true.
For example, you may want to turn on a device, but only if
it is safe to do so.
We call this kind of transition a *guarded transition*.
To specify this transition, you define a *guard*, which
is an abstract function that returns a Boolean value.
Then you use the guard in a transition.
Here is an example:
[source,fpp]
----
@ A device state machine with a guarded transition
state machine Device {
@ A guard for checking whether the device is in a safe state for power-on
guard powerOnIsSafe
@ A signal for turning the device on
signal cmdOn
@ A signal for turning the device off
signal cmdOff
@ The initial state is OFF
initial enter OFF
@ The ON state
state ON {
@ In the ON state, a cmdOff signal causes a transition to the OFF state
on cmdOff enter OFF
}
@ The OFF state
state OFF {
@ In the OFF state, a cmdOff signal causes a transition to the ON state
@ if powerOnIsSafe evaluates to true. Otherwise no transition occurs.
on cmdOn if powerOnIsSafe enter ON
}
}
----
Here is the graphical representation:
image::users-guide/diagrams/state-machine/GuardedTransitions.png[Device state machine with a guarded transition,400,align="center"]
In this example, there is one guard, `powerOnIsSafe`.
The implementation of this function will return true
if it is safe to power on the device; otherwise it will
return false.
In state `OFF`, the transition on signal `cmdOn` is
now guarded: when the signal is received in this state,
the transition occurs if and only if `powerOnIsSafe`
evaluates to `true`.
As with actions, each guard must have a unique name.
Also as with actions, a guard can have a type; if it does,
the type must match the type of the signal at the point
where the guard is evaluated.
For example, here is a revised version of the previous
state machine that adds a value of type `DeviceStatus`
to the guard `powerOnIsSafe`:
[source,fpp]
----
@ A type representing the status of a device
type DeviceStatus
@ A device state machine with a guarded transition
state machine Device {
@ A guard for checking whether the device is in a safe state for power-on
@ The DeviceStatus value provides the current device status
guard powerOnIsSafe: DeviceStatus
@ A signal for turning the device on
@ The DeviceStatus value provides the current device status
signal cmdOn: DeviceStatus
@ A signal for turning the device off
signal cmdOff
@ The initial state is OFF
initial enter OFF
@ The ON state
state ON {
@ In the ON state, a cmdOff signal causes a transition to the OFF state
on cmdOff enter OFF
}
@ The OFF state
state OFF {
@ In the OFF state, a cmdOff signal causes a transition to the ON state
@ if powerOnIsSafe evaluates to true. Otherwise no transition occurs.
on cmdOn if powerOnIsSafe enter ON
}
}
----
When you send the signal `cmdOn` to an instance of this state
machine, you must provide a value of type `DeviceStatus`.
When the state machine instance evaluates the guard `powerOnIsSafe`,
it passes in the value as an argument to the function.
==== Self Transitions
When a state transition has the same state _S_ as its source
and its target, and _S_ has no substates, we call the transition
a *self transition*.
In this case the following behavior occurs:
* The state machine does the exit actions for _S_, if any.
* The state machine does the actions specified in the transition, if any.
* The state machine does the entry actions for _S_, if any.
Note that on a self transition, the state machine exits and
reenters _S_.
This behavior is a special case of a more general behavior that we will
discuss
<<Defining-State-Machines_Hierarchy_Entry-and-Exit-Actions,below>>
in connection with state hierarchy.
<<Defining-State-Machines_Hierarchy_Directly-Related-States,Below>>
we will also generalize the concept of a self transition to
the case of a state with substates.
As an example, consider the following state machine:
[source,fpp]
----
@ A state machine representing a device with on-off behavior,
@ with a self transition
state machine Device {
@ An action on entering the ON state
action enterOn
@ An action to perform on reset
action reset
@ An action on exiting the ON state
action exitOn
@ A signal for turning the device on
signal cmdOn
@ A signal for turning the device off
signal cmdOff
@ A signal for resetting the device
signal cmdReset
@ The initial state is OFF
@ Before entering the initial state, do initialAction1 and then initialAction2
initial enter OFF
@ The ON state
state ON {
@ On entering the ON state, do enterOn
entry do { enterOn }
@ In the ON state, a cmdOff signal causes a transition to the OFF state
on cmdOff enter OFF
@ In the ON state, a cmdReset signal causes a self transition
on cmdReset do { reset } enter ON
@ On exiting the ON state, do exitOn
exit do { exitOn }
}
@ The OFF state
state OFF {
@ In the OFF state, a cmdOff signal causes a transition to the ON state
on cmdOn enter ON
}
}
----
Here is the graphical representation:
image::users-guide/diagrams/state-machine/SelfTransitions.png[Device state machine with a self transition,400,align="center"]
In this example, when the state machine is in the `ON` state and
it receives a `cmdReset` signal, the following behavior occurs:
* Do action `exitOn`.
* Do action `reset`.
* Do action `enterOn`.
==== Internal Transitions
An *internal transition* is like a
<<Defining-State-Machines_More-on-State-Transitions_Self-Transitions,self transition>>,
except that there is no exit and reentry.
To write an internal transition, you write the `on` and `do`
parts of a transition and omit the `enter` part.
For example, here is a device state machine with a `reset`
action that causes an internal transition:
[source,fpp]
----
@ A state machine representing a device with on-off behavior,
@ with an internal transition
state machine Device {
@ An action on entering the ON state
action enterOn
@ An action to perform on reset
action reset
@ An action on exiting the ON state
action exitOn
@ A signal for turning the device on
signal cmdOn
@ A signal for turning the device off
signal cmdOff
@ A signal for resetting the device
signal cmdReset
@ The initial state is OFF
@ Before entering the initial state, do initialAction1 and then initialAction2
initial enter OFF
@ The ON state
state ON {
@ On entering the ON state, do enterOn
entry do { enterOn }
@ In the ON state, a cmdOff signal causes a transition to the OFF state
on cmdOff enter OFF
@ In the ON state, a cmdReset signal causes an internal transition
on cmdReset do { reset }
@ On exiting the ON state, do exitOn
exit do { exitOn }
}
@ The OFF state
state OFF {
@ In the OFF state, a cmdOff signal causes a transition to the ON state
on cmdOn enter ON
}
}
----
Here is the graphical representation:
image::users-guide/diagrams/state-machine/InternalTransitions.png[Device state machine with an internal transition,200,align="center"]
In this example, when the state machine is in state `ON` and it
receives a `cmdReset` signal, it does the `reset` action and
performs no other behavior.
An internal transition may have a guard.
For example, we could define a guard `resetIsSafe` and
write the internal transition as follows:
[source,fpp]
--------
on cmdReset if resetIsSafe do { reset }
--------
As with other transitions, if the signal carries data, then
any actions and guards names in an internal transition may
carry data of a compatible type.
=== Choices
A *choice definition* is a state machine member that
defines a branch point for one or more transitions.
In this section we explain how to write and use
choice definitions.
*Basic choice definitions:*
The most basic choice definition consists of the following:
* A name. Like a state name, this name can be the target
of a transition.
* The name of a
<<Defining-State-Machines_More-on-State-Transitions_Guarded-Transitions,guard>>
_G_.
The evaluation of _G_ selects which branch of the choice to
follow.
* An *if transition* that specifies what to do if _G_
evaluates to `true`.
* An *else transition* that specifies what to do if _G_
evaluates to `false`.
Each of the if and else transitions has a target,
which can be a state or a choice.
Here is an example:
[source,fpp]
----
@ A device state machine with a choice
state machine Device {
@ A guard for checking whether the device is in a safe state for power-on
guard powerOnIsSafe
@ A signal for turning the device on
signal cmdOn
@ A signal for turning the device off
signal cmdOff
@ A signal for resetting the device
signal cmdReset
@ The initial state is OFF
initial enter OFF
@ The ON state
state ON {
@ In the ON state, a cmdOff signal causes a transition to the OFF state
on cmdOff enter OFF
}
@ The OFF state
state OFF {
@ In the OFF state, a cmdOff signal causes a transition to the choice ON_OR_ERROR
on cmdOn enter ON_OR_ERROR
}
@ The ON_OR_ERROR choice
choice ON_OR_ERROR {
if powerOnIsSafe enter ON else enter ERROR
}
@ The ERROR state
state ERROR {
@ In the ERROR state, a cmdReset signal causes a transition to the OFF state
on cmdReset enter OFF
}
}
----
Here is the graphical representation:
image::users-guide/diagrams/state-machine/Choices.png[Device state machine with a choice,450,align="center"]
This version of the `Device` state machine has three states:
`ON`, `OFF`, and `ERROR`.
It also has a choice `ON_OR_ERROR`.
Each instance of the state machine has the following behavior:
* The initial state is `OFF`.
* On receiving the signal `cmdOn` in the `OFF` state, it
enters the choice `ON_OR_ERROR`.
At that point, if `powerOnIsSafe` evaluates to `true`, then
it enters then `ON` state.
Otherwise it enters the `ERROR` state.
* On receiving the signal `cmdReset` in the `ERROR` state,
it enters the `OFF` state.
* On receiving the signal `cmdOff` in the `ON` state,
it enters the `OFF` state.
The text inside the curly braces of a choice consists
of a single line.
To write the text on multiple lines, you can use an
<<Defining-Constants_Multiline-Definitions,explicit line
continuation>>. For example:
[source,fpp]
--------
choice ON_OR_ERROR {
if powerOnIsSafe \
enter ON \
else enter ERROR
}
--------
*Initial transitions to choices:*
An initial transition can go to a choice.
This pattern can express conditional behavior on state
machine startup.
For example, in the `Device` state machine shown above,
we could have the initial transition go to a choice
that checks a safety condition and then enters either
the `OFF` state or an error state.
*Choice transitions to choices:*
An if transition or an else transition of a choice
(or both) can enter another choice.
For example, it is permissible to write a chain of
choices like this:
[source,fpp]
--------
choice C {
if g1 enter C1 else enter C2
}
choice C1 {
if g2 enter S1 else enter S2
}
choice C2 {
if g3 enter S3 else enter S4
}
--------
Effectively this is a four-way choice; it is a two-way
choice, each branch of which leads to a two way choice.
By having the if or else branch of `C1` go directly
to a state, you could get a three-way choice.
And by adding more levels, you can create an _n_ -way
choice for any _n_.
In this way you can use choices to create arbitrarily
complex branching patterns.
Note though, that it is usually a good idea not to have
more than a few levels of choices; otherwise the state
machine can be complex and hard to understand.
*The type associated with a choice:*
Like initial transitions and state transitions, the
transitions out of a choice may
<<Defining-State-Machines_Actions_Typed-Signals-and-Actions,
carry a value>>.
To determine whether the transitions of a choice _C_ carry a value,
and if so what type that value has, we use the following rules:
. If any transition into _C_ carries no value,
then transitions out of _C_ carry no value.
. Otherwise if all of the transitions into _C_ carry
a value of the same type _T_, then each of the
transitions out of _C_ carries a value of type _T_.
. Otherwise if the incoming types can be resolved to
a common type _T_, then each of the transitions out of _C_
carries a value of type _T_.
The rules for resolving common types are given in
_The FPP Language Specification_.
The basic idea is to find a type to which it is always
safe to cast all the incoming types.
. Otherwise the analyzer reports an error.
*Actions in choice transitions:*
You can do actions in choice transitions just as for
<<Defining-State-Machines_Actions,initial transitions and
state transitions>>.
For example, suppose we add the definitions of actions `onAction` and
`errorAction` to the `Device` state machine shown above.
Then we could revise the `ON_OR_ERROR` choice to read as follows:
[source,fpp]
--------
choice ON_OR_ERROR {
if powerOnIsSafe do onAction enter ON else do errorAction enter ERROR
}
--------
As for other kinds of transitions, the actions done in choice
transitions may carry values.
If an action _A_ done in a transition of a choice _C_ carries a value,
the type named in the definition of _A_ must be
compatible with the type associated with _C_, as discussed above.
*Entry and exit actions:*
When a transition _T_ of a state machine _M_
goes to or from a choice, entry and
exit actions are done as if each choice were a state with
no entry and or exit actions.
For example, let _I_ be an instance of _M_, and
suppose _I_ is carrying out a transition _T_.
* If _T_ goes from a choice _C_ to a state _S_,
then _I_
does the actions of _T_ followed by the entry actions of _S_.
* If _T_ goes from a state _S_ to a choice _C_,
then _I_
does the exit actions of _S_ followed by the actions of _T_.
* If _T_ goes from a choice _C_~1~ to a choice _C_~2~,
then _I_ does the actions of _T_.
*Rules for choice definitions:*
Choice definitions must conform to the following rules:
* No state or choice may have the same name as any other
state or choice.
* Every choice must be reachable from the initial transition
or from a state transition.
* There may be no cycles of choice transitions.
For example, it is not permitted for a choice `C1` to
have a transition to a choice `C2` that has a transition
back to `C1`.
Nor is it permissible for `C1` to go to `C2`, `C2` to
go to `C3`, and `C3` to go to `C1`.
=== Hierarchy
As with UML state machines, FPP state machines can have *hierarchy*.
That is, we can do the following:
* Define states within other states.
When a state _T_ is defined within a state _S_, _S_ is called the
*parent* of _T_, and _T_ is called a *substate* or *child* of _S_.
* Define choices within states.
Using hierarchy, we can do the following:
* Group related substates under a single parent.
This grouping lets us express the state machine structure in a
disciplined and modular way.
* Define behaviors of a parent state that are inherited
by its substates.
The parent behavior saves having to redefine the behavior for each
substate.
* Control the way that entry and exit actions occur
when transitions cross state boundaries.
A state machine with hierarchy is called a
*hierarchical state machine*.
In the following subsections, we explain how to define
hierarchical state machines in FPP.
==== Substates
In this section we explain how to define and use substates.
*An example:*
Here is an example of a state machine with substates:
[source,fpp]
----
@ A device state machine with substates
state machine Device {
@ A signal for turning the device on
signal cmdOn
@ A signal for turning the device off
signal cmdOff
@ A signal for indicating that the device is in an unsafe state
signal cmdUnsafe
@ A signal for indicating that the device is in a safe state
signal cmdSafe
@ The initial state is OFF
initial enter OFF
@ The ON state
state ON {
@ In the ON state, a cmdOff signal causes a transition to the OFF state
on cmdOff enter OFF
@ In the ON state, a cmdUnsafe signal causes a transition to OFF.UNSAFE
on cmdUnsafe enter OFF.UNSAFE
}
@ The OFF state
state OFF {
@ The initial state is SAFE
initial enter SAFE
@ The state OFF.SAFE
@ In this state, it is safe to turn on the device
state SAFE {
@ In the SAFE state, a cmdOff signal causes a transition to the ON state
on cmdOn enter ON
@ In the SAFE state, a cmdUnsafe signal causes a transition to the UNSAFE state
on cmdUnsafe enter UNSAFE
}
@ The state OFF.UNSAFE
@ In this state, it is not safe to turn on the device
state UNSAFE {
@ In the UNSAFE state, a cmdSafe signal causes a transition to the SAFE state
on cmdSafe enter SAFE
}
}
}
----
Here is the graphical representation:
image::users-guide/diagrams/state-machine/Substates.png[Device state machine with substates,500,align="center"]
This state machine has four states: `ON`, `OFF`, `OFF.SAFE`, and `OFF.UNSAFE`.
The last two states are substates of `OFF`.
Notice the following:
* The substates are defined syntactically within the parent state.
* The full names of the substates are qualified by the name of the parent state.
* Inside the scope of the parent state, you can refer
to the substates by the shorter name that omits the implied qualifier.
The way the qualification works for state names is identical to the
way it works for <<Defining-Modules,module names>>.
An instance _m_ of this state machine has the following behavior:
. When _m_ starts up, it runs its initial transition specifier, just as for
<<Defining-State-Machines_States-Signals-and-Transitions, a state machine
without hierarchy>>.
The state machine enters state `OFF`.
`OFF` is a parent state, so it in turn has an initial transition
specifier which is run.
The state machine enters `OFF.SAFE`.
. In state `ON`, the following behavior occurs:
.. When _m_ receives signal `cmdOff`, it enters state
`OFF`. This entry causes it to enter state `OFF.SAFE` as discussed above.
.. When _m_ receives signal `cmdUnsafe`, it goes
directly to `OFF.UNSAFE`, bypassing the `OFF` state and its initial
transition.
. In state `OFF.SAFE`, the following behavior occurs:
.. When _m_ receives signal `cmdOn`, it enters
the `ON` state.
.. When _m_ receives signal `cmdUnsafe`, it
enters the `OFF.UNSAFE` state.
. In state `OFF.UNSAFE`, when _m_ receives signal
`cmdSafe`, it enters the `OFF.SAFE` state.
*Rules for substates:*
Here are the rules for defining substates:
. Each parent state _S_ must have exactly one initial transition
specifier that enters a substate of _S_.
. Each state, including parents and substates, must be reachable
from the initial transition of the state machine or from
a state transition.
. Substates may themselves be parents (i.e., may have substates),
to any depth.
Rule 1 ensures that the final target of every transition,
after following all initial transition specifiers, is a
*leaf state*, i.e., a state that has no substates.
*The hierarchy tree:*
When a state _S_ is a parent of a state _S'_, we say that _S_ is an
*ancestor* of _S'_ in the state hierarchy.
We also say that _S_ is an ancestor of _S'_ if it is a parent of
an ancestor of _S'_.
For example, if _S_ is a parent of a parent of _S'_, then
_S_ is an ancestor of _S'_.
Note that a state is never an ancestor of itself.
When a state _S_ is a an ancestor of a state _S'_, we
say that _S'_ is a *descendant* of _S_.
For example, _S'_ is a descendant of _S_ if _S'_
is a child of _S_, or if _S'_ is a child of a child of
_S_.
Note that a state is never a descendant of itself.
In order to make the state hierarchy into a tree, we also
say that the entire state machine _M_ is a parent of every top-level
state in the state machine.
This means that (1) _M_ is an ancestor of every
state in _M_ and (2) every state in _M_ is a descendant of _M_.
We will say that the tree constructed in this way is the
*hierarchy tree* for _M_, and that each of _M_ and every state
in _M_ is a *node* in the hierarchy tree.
In particular, _M_ is the root node of the hierarchy tree.
==== Inherited Transitions
In general, when a transition _T_ is defined in a parent state _S_,
_T_ behaves as if it were defined in each of the
<<Defining-State-Machines_Hierarchy_Substates, leaf states>> that is a
<<Defining-State-Machines_Hierarchy_Substates, descendant of _T_>>.
In this case we say that _T_ is *inherited* by each of the leaf states.
There is an important exception to this rule:
When a state _S_ defines a transition _T_ on a signal _s_,
and a descendant _S'_ of _S_ defines another transition _T'_
on the same signal _s_, the behavior of _T'_ takes precedence
over the inherited transition _T_ in the behavior of _S'_.
This rule is called *behavioral polymorphism* for transitions.
*Example:*
Here is an example that illustrates inherited transitions
and behavioral polymorphism:
[source,fpp]
----
@ A device state machine with inherited transitions and behavioral polymorphism
state machine Device {
@ A signal for turning the device on
signal cmdOn
@ A signal for turning the device off
signal cmdOff
@ A signal for indicating that the device is in an unsafe state
signal cmdUnsafe
@ A signal for indicating that the device is in a safe state
signal cmdSafe
@ The initial state is DEVICE
initial enter DEVICE
@ The DEVICE state
state DEVICE {
@ The initial state is OFF
initial enter OFF
@ In the DEVICE state, a cmdUnsafe signal causes a transition to OFF.UNSAFE
on cmdUnsafe enter OFF.UNSAFE
@ The ON state
state ON {
@ In the ON state, a cmdOff signal causes a transition to the OFF state
on cmdOff enter OFF
}
@ The OFF state
state OFF {
@ The initial state is SAFE
initial enter SAFE
@ The state OFF.SAFE
@ In this state, it is safe to turn on the device
state SAFE {
@ In the SAFE state, a cmdOff signal causes a transition to the ON state
on cmdOn enter ON
}
@ The state OFF.UNSAFE
@ In this state, it is not safe to turn on the device
state UNSAFE {
@ In the UNSAFE state, a cmdSafe signal causes a transition to the SAFE state
on cmdSafe enter SAFE
@ In the UNSAFE state, a cmdUnsafe signal causes no action
on cmdUnsafe do { }
}
}
}
}
----
Here is the graphical representation:
image::users-guide/diagrams/state-machine/InheritedTransitions.png[Device state machine with inherited transitions,600,align="center"]
Here we have rewritten the
<<Defining-State-Machines_Hierarchy_Substates,
`Device` state machine from the previous section>>
so that all the states in that example are descendants of a single state
`DEVICE`.
By doing this, we can have a single transition out of `DEVICE` on signal
`cmdUnsafe`.
Before we had to write out the same transition twice,
once in the state `ON` and once in the state `OFF.SAFE`.
Here we can write the transition once in the ancestor state, and
it is inherited by all the descendants.
There is one catch, though: in the previous example, we did not define
the transition on `cmdUnsafe` in the state `OFF.UNSAFE`.
Here, if we use inheritance in the obvious way, the transition will
be inherited by all the descendants of `DEVICE`, including `OFF.UNSAFE`,
so the behavior will not be exactly the same as for the previous
state machine.
This may not matter much in this example, but it would matter if the
the state `DEVICE.OFF.UNSAFE` had entry or exit actions; in this case
transition from `UNSAFE` to itself (which is a
<<Defining-State-Machines_More-on-State-Transitions_Self-Transitions,self transition>>)
would cause an exit from and reentry to the state, which we may not want.
To remedy this situation, we use behavioral polymorphism.
In the state `DEVICE.OFF.UNSAFE`, we define an
<<Defining-State-Machines_More-on-State-Transitions_Internal-Transitions,internal
transition>>
that has an empty list of actions and so does nothing.
This transition overrides the transition provided in the ancestor state,
so it restores the behavior that, on receipt of the signal
`cmdUnsafe` in the state `DEVICE.OFF.UNSAFE`, nothing happens.
*Syntactic and flattened state transitions:*
Once we introduce substates and inheritance, it is useful to
distinguish *syntactic state transitions* from *flattened state transitions*.
A syntactic state transition is a state transition in the FPP source
for a state machine _M_.
A flattened state transition is a transition that results from
applying the rules for transition inheritance to a syntactic
state transition.
We say the transition is "`flattened`" because we create
it by moving the left side of the transition down to a leaf state.
This move flattens the hierarchy on the left
side of the transition.
When there is no hierarchy, a syntactic transition from state
_S_~1~ to _S_~2~ generates exactly one flattened transition,
also from _S_~1~ to _S_~2~.
Once we have hierarchy, however, syntactic and flattened
state transitions may be different.
For example, suppose that _S_~1~ is a parent state,
and let _T_ be a syntactic transition from _S_~1~ to _S_~2~.
Then for each descendant _L_ of _S_~1~ that is a leaf state, there is a flattened
state transition from _L_ to _S_~2~.
Note in particular that whereas a syntactic state transition may
have a parent state as its source, a flattened state transition always
has a leaf state as its source.
This distinction between syntactic and flattened state transitions
will be useful when we discuss entry and exit actions
in the following sections.
*Internal transitions:*
<<Defining-State-Machines_More-on-State-Transitions_Internal-Transitions,
Internal transitions>> are flattened and inherited like other transitions,
except that there is no target state.
When a parent state _S_ defines an internal transition, the following occurs:
* There is one flattened transition for each leaf state that
that is a descendant of _S_.
* The behavior of each flattened transition is to stay in _S_
and do the actions listed in the transition.
There is no state change, and no entry or exit actions are done.
* As usual, any of these flattened transitions may be overridden
by behavioral polymorphism.
==== Entry and Exit Actions
In previous sections on
<<Defining-State-Machines_Actions_Entry-and-Exit-Actions,
entry and exit actions>> and on
<<Defining-State-Machines_More-on-State-Transitions_Self-Transitions,
self transitions>>,
we explained the order in which actions occur during a transition between
states without hierarchy.
Each of the behaviors described there is a special case of a more general
behavior for state machines with hierarchy, which we now describe.
*General behavior:*
Suppose, in the course of running an instance of a state machine _M_,
a <<Defining-State-Machines_Hierarchy_Inherited-Transitions,
flattened state transition>> _T_
occurs from state _L_ to state _S_.
By the definition of a flattened state transition, we know
that _L_ is a leaf state.
When carrying out the transition _T_, the state machine instance will do
actions as follows:
. Compute the *least common ancestor* of _L_ and _S_.
This is the unique node _N_ of the <<Defining-State-Machines_Hierarchy_Substates,
hierarchy tree>> of _M_ such that (a) _N_ is an ancestor of _L_,
(b) _N_ is an ancestor of _S_, and (c) there is no node _N'_
that is a descendant of _N_ and that satisfies properties (a) and (b).
. Traverse the hierarchy tree upwards from _L_ to _N_.
At each point where the traversal passes out of a state _S'_, in the
order of the traversal, do the exit actions of _S'_, if any.
. Do the actions specified in _T_, if any.
. Traverse the hierarchy tree downwards from _N_ to _S_.
At each point where the traversal enters a state _S'_, in the
order of the traversal, do the entry actions of _S'_, if any.
For example, suppose that _M_ has a state _A_ with substates
_B_ and _C_, _B_ has substate _D_, and _C_ has substate _E_.
Suppose that _T_ goes from _C_ to _E_.
Then least common ancestor is _A_, and the following actions
would be done, in the following order:
the exit actions of _C_, the exit actions of _B_,
the actions of _T_, the entry actions of _C_, and the
entry actions of _E_.
Remember also that if _E_ is not a leaf state, then _T_ will
<<Defining-State-Machines_Hierarchy_Substates,follow
one or more transitions to go to a leaf state>>.
In this case, any actions specified in those transitions are
done as well,
after the transitions described above, and in the order that
the initial transitions are run.
Finally, the algorithm above is described in a way
that emphasizes ease of understanding.
As stated, it is inefficient because it
recomputes the least common ancestor every time a flattened state
transition occurs.
In fact all the sequences of exit and entry actions for flattened
state transitions can be computed before generating code, and
it is more efficient to do this.
This is how the FPP code generator works.
For more details, see the algorithm descriptions on the
https://github.com/nasa/fpp/wiki/Compute-Flattened-State-Transition-Map[FPP wiki].
*The special case of no hierarchy:*
The general behavior described above agrees with the special-case
behavior that we described in the section on
<<Defining-State-Machines_Actions_Entry-and-Exit-Actions,
entry and exit actions for state machines without hierarchy>>.
When a state machine _M_ has no hierarchy, a every state transition _T_
is a flattened transition that goes from a leaf state _L_ to a leaf
state _S_, both of which are children of _M_ in the hierarchy tree.
So we always exit _L_, do the actions of _T_, and enter _S_.
In particular,
the general behavior agrees with the behavior that we previously
described for
<<Defining-State-Machines_More-on-State-Transitions_Self-Transitions,
self transitions>>.
When _L_ and _S_ are the same leaf state, the least common
ancestor of _L_ and _S_ is the parent _P_ of _S_.
So we exit _S_ to go up to _P_, do the actions of _T_, and
reenter _S_.
==== Directly Related States
Let _S_~1~ and _S_~2~ be states.
If _S_~1~ is equal to S~2~, or _S_~1~ is an ancestor of S~2~, or S~2~
is an ancestor of _S_~1~, then we say that _S_~1~ and S~2~ are
*directly related* in the hierarchy tree.
In this section we describe the behavior of transitions between
directly related states.
Each of the behaviors described below follows from the general behavior
presented in the previous section.
However, in some cases the behavior may be subtle or surprising.
*Flattened transitions to ancestors:*
When a transition _T_ goes from a leaf state _L_ to a state _A_
that is an ancestor of _L_, we call _T_ a
*flattened transition to an ancestor*.
The least common ancestor
of _L_ and _A_ is the parent _P_ of _A_.
Therefore the following behavior occurs:
. Do exit actions to get from _L_ to _P_. The last actions will
be the exit actions of _A_, if any.
. Do the actions of _T_, if any.
. Do the entry actions of _A_, if any.
*Syntactic transitions to ancestors:*
Consider a state transition _T_ of the form
`on` _s_ `enter` _A_ that is defined in the state _S_,
where _A_ is an ancestor of _S_.
We call this kind of state transition a
*syntactic transition to an ancestor*.
If _S_ is a leaf state, then it represents
a flattened transition to the ancestor _A_.
Otherwise it represents one flattened transition
to the ancestor _A_ for each descendant of _S_ that
is a leaf state.
Because of
<<Defining-State-Machines_Hierarchy_Inherited-Transitions,
behavioral polymorphism>>, any of the flattened transitions may
be overridden.
*Flattened transitions to self:*
A *flattened transition to self* is a transition
from a leaf state _L_ to itself.
This is what we previously called a
<<Defining-State-Machines_More-on-State-Transitions_Self-Transitions,self
transition>>.
*Syntactic transitions to self:*
Consider a state transition _T_ of the form
`on` _s_ `enter` _S_ that is defined in the state _S_.
In general we call this kind of state transition a
*syntactic transition to self*.
If _S_ is a leaf state, then _T_ a flattened
transition to self.
In particular, when there is no hierarchy, every syntactic
transition to self is a self transition.
If _S_ is not a leaf state, then _T_ is flattened
to one or more transitions from leaf states _L_ that are
descendants of _S_.
Each of these transitions is a flattened transition
to the ancestor _S_.
Because of
<<Defining-State-Machines_Hierarchy_Inherited-Transitions,
behavioral polymorphism>>, any of the flattened transitions may
be overridden.
*Flattened transitions to descendants:*
In theory, a *flattened transition to a descendant* would
be a transition from a leaf node _L_ to a descendant _D_ of _L_.
However, leaf nodes have no descendants, so no such transition
is possible.
We include the category for completeness.
It has no members.
*Syntactic transitions to descendants:*
Consider a state transition _T_ of the form
`on` _s_ `enter` _D_ that is defined in the state _S_,
where _D_ is a descendant of _S_.
We call this kind of state transition a
*syntactic transition to a descendant*.
By symmetry with syntactic transitions to ancestors,
you might expect that the first behavior when
making such a transition is to exit and reenter _S_.
However, this is not what happens.
Instead, _T_ represents one flattened transition
from each leaf state that is a descendant of _S_.
The flattened transitions have the following properties:
. If _D_ is a leaf state, then the flattened
transition out of _D_ (and only that transition)
is a flattened transition to self.
. Otherwise (a) none of the flattened transitions
is a flattened transition to self, and (b) the
flattened transitions out of the descendants of _D_
are flattened transitions to the ancestor _D_.
In either case, because of
<<Defining-State-Machines_Hierarchy_Inherited-Transitions,
behavioral polymorphism>>, any of the flattened transitions may
be overridden.
==== Choices
Like state definitions, choice definitions are hierarchical.
That is, you may define a choice inside a state.
The names of choices are qualified by the enclosing
state names
<<Defining-State-Machines_Hierarchy_Substates,as for the
names of substates>>.
For example, you can write this:
[source,fpp]
--------
state machine M {
...
state S {
...
choice C { ... }
...
}
}
--------
The dots represent omitted text needed to make the state machine valid.
In this example, the qualified name of the choice is `S.C`.
Inside state `S`, you can refer to the choice as `S.C` or `C`.
Outside state `S`, you must refer to it as `S.C`.
*Initial transitions to choices:*
When an initial transition _I_ goes to a choice _C_,
_C_ and _I_ must have the same parent _P_ in the
<<Defining-State-Machines_Hierarchy_Substates,hierarchy tree>>.
In addition, each transition out of _C_ must go to a state
or choice with parent _P_; and if it goes
to a choice, then each transition out of that choice must
go to a state or choice with parent _P_, and so forth.
Another way to say this is that (since no cycles of transitions
through choices are allowed) each transition
path out of _I_ must go through zero or more choices with parent _P_
to a state with parent _P_.
For example, this state machine is allowed:
[source,fpp]
----
state machine ValidInitialChoice {
guard g
initial enter S1
state S1 {
@ This initial transition is valid: C, S2, and S3 all have parent S1
initial enter C
choice C { if g enter S2 else enter S3 }
state S2
state S3
}
}
----
But this one is not:
[source,fpp]
--------
state machine InvalidInitialChoice {
guard g
initial enter S1
state S1 {
@ This initial transition is invalid: C has parent S1,
@ but S2 and S3 have parent InvalidInitialChoice
initial enter C
choice C { if g enter S2 else enter S3 }
}
state S2
state S3
}
--------
*Entry and exit actions:*
As in the <<Defining-State-Machines_Choices,non-hierarchical case>>,
when a transition _T_ of a state machine _M_
goes to or from a choice, entry and
exit actions occur as if each choice were a leaf state with
no entry or exit actions.
For example, suppose that _M_ has a state _S_ with substates
_A_ and _B_, _A_ has a choice _C_, and _B_ has substate _B'_.
Suppose that _T_ goes from _C_ to _B'_.
Then least common ancestor is _S_, and the following actions
would be done, in the following order:
the exit actions of _A_, the actions of _T_, the entry actions of _B_, and the
entry actions of _B'_.