rFSM is a small and powerful Statechart implementation. It is mainly designed for Coordination of complex systems but is not limited to that. rFSM is written in pure Lua and is therefore highly portable and embeddable. As a Lua domain specific language rFSM inherits the extensibility of its host language.
Make sure you have Lua 5.1 or 5.2 installed and the rFSM folder is in
your LUA_PATH
. For example:
export LUA_PATH=";;;/home/mk/src/git/rfsm/?.lua"
If your LUA_PATH
is already set to something, then just add the rFSM
path instead of overwriting it:
export LUA_PATH="$LUA_PATH;/home/mk/src/git/rfsm/?.lua"
rFSM is minimal Statechart flavor designed for Coordinating of complex systems such as robots. It has the following features:
- Hierarchical (composite) states
- Completion events
- Parametrizable and reusable states
- Easy to build statemachines by composing existing states/state machines
- Plugin mechanism permits extending the core engine. Available plugins include timeevents, event memory, sequential AND states and more.
- Real-time safe operation possible using lua-tlsf/rtp1
The following shows a simple hello_world example
1 return rfsm.state {
2 hello = rfsm.state { exit=function() print("hello") end },
3 world = rfsm.state { entry=function() print("world") end },
4
5 rfsm.transition { src='initial', tgt='hello' },
6 rfsm.transition { src='hello', tgt='world', events={ 'e_done' } },
7 rfsm.transition { src='world', tgt='hello', events={ 'e_restart' } },
8 }
The first line defines a new toplevel composite state and returns
it. The root state of an rFSM state machine is always a state
itself. This permits it to be composed as a substate in a different
state machine. The return
statement facilitates reading rfsm model
files by tools or other state machines.
The second and third line define two leaf states that are part of the
toplevel composite state. hello
defines an exit function and world
an entry function which are called when the state is exited/entered,
respectively.
The next three lines define transition between these states. The first
is from the initial connector to the hello
state. This transition
will be taken the first time the composite state is entered. The
initial connector, as an exception, need not be defined and will be
created automatically when referenced from a transition.
The next transition is from hello
to world
and is triggered by the
e_done
event. This event is raised internally when a state
completes, which is either the case when the states 'doo' function
(see below) finishes or immediately, if there is no doo
, as is the
case here. The third transition is triggered by the e_restart
event.
Next we execute this statemachine in the rfsm-simulator:
$ tools/rfsm-sim examples/hello_world.lua
Lua 5.1.4 Copyright (C) 1994-2008 Lua.org, PUC-Rio
rFSM simulator v0.1, type 'help()' to list available commands
INFO: created undeclared connector root.initial
> step()
hello
active: root.hello(done)
queue: e_done@root.hello
We execute step()
to advance the state machine once. As this is the
first step, the fsm is entered via the 'initial' connector to the
hello
state. After that the state hello
is active and done
(because no doo
function is defined). Consequently, an e_done
completion event has been generated and placed in the queue. So the
next step:
> step()
world
active: root.world(done)
queue: e_done@root.world
... causes a transition to world
. As the world
state completion
event does not trigger any transitions, running step()
again does
not have any effect:
> step()
active: root.world(done)
queue:
But we can manually send in the e_restart
event and call step()
,
which takes us back to hello
:
> se("e_restart")
> step()
hello
active: root.hello(done)
queue: e_done@root.hello
rFSM state machines are constructed using three model elements: states, connectors and transitions.
(all functions are part of the rfsm module, thus need to be called in
Lua with the rfsm
prefix, e.g. rfsm.state{}
)
States are used to model discrete states of the system and can be
either composite or leaf states. A composite state contains other
states, while a leaf state does not. States can define entry
and
exit
functions
entry(fsm, state, 'entry')
exit(fsm, state, 'exit')
that are called when the state is entered or exited respectively. The arguments passed in are the toplevel statechart, the current state and the string 'entry' resp. 'exit'. Normally you don't need these arguments and should not change them either. (The rationale behind the second and third argument is to permit one function to handle entry and exit of multiple states and hence needs to identify these).
Leaf states may additionally define a do function (it is called doo
in rFSM to avoid clashes with the identically named Lua keyword).
bool doo(fsm, state, 'doo')
The doo function is used to perform actions while a leaf state is active. To that end it can be used such that it is repeatedly called until either the function completes or an event triggers a transition to a different state.
Implementationwise, this function is treated as a Lua coroutine. This enables the following two use-cases:
-
doo
is a regular function:doo
is executed once and a completion evente_done
is raised afterwards (if nodoo
function is defined this event is raised immediately after execution of theentry
function). -
Long running
doo
with voluntary preemption: while possible, it is not recommended to define adoo
function that runs for a longer time, because this would prevent incoming events to trigger transitions. Therefore, therfsm.yield()
call can be inserted at appropriate points into a long runningdoo
to explicitly return control to the rfsm engine, that then checks for new events and potentially executes transitions.
(Note: rfsm.yield is currently only an alias to coroutine.yield
)
The following example illustrates the second use case:
doo = function(fsm)
while true do
if min_distance() < 0.1 then
rfsm.send_events(fsm, "e_close_obj")
end
rfsm.yield()
end
end
This doo
will check a certain condition repeatedly and raise the
e_close_obj
event if it is true. Each cycle the control is returned
to the rFSM core by calling rfsm.yield()
.
rfsm.yield(idle_flag)
accepts a Boolean argument (called the "idle
flag") that influences how doo
is called by the rFSM core: if true
it will cause the rFSM core to go idle, provided there are no other
events. If false
(the default2 if no
arguments are given) and there are no other events, doo
will be
called in a tight loop. It depends on each application which
idle_flag
is appropriate. In general the idle_flag should always be
true unless the intention is that the doo
function is executed as
fast as possible (potentially consuming a lot of CPU!).
The root composite state honors some extra fields to refine the global FSM behavior.
Configuring error, warning, informational and debug output. The
err
, warn
, info
and dbg
fields can be used to fine tune how
these messages are output. The value of these fields can be either
true or false or set to a function that accepts a variable list of
arguments. The default is to write errors and warnings to stderr and
info to stdout. Debug messages are turned off by default. Nicer and
configurable pretty printing of debug output is provided by the
rfsmpp
module (described below).
The getevents
hook. The getevents
hook is called by the
rFSM core whenever it needs to check for new events. This function is
the central mechanism to integrate rFSM into existing systems. The
expected behavior is to return a Lua table of events (array part
only). These events are then used to check for enabled transitions.
Transitions define how a state machine changes state upon receiving events:
Example:
rfsm.transition {
src='stateX', tgt='stateY', events = {"e1", "e2"},
guard=function()
if getVal() > 0.3 then
return false
end
return true
end,
effect=function () do_this() end
}
The above defines a transition between stateX and stateY which is
triggered by the events e1
and e2
. The guard
condition
(optional) will prevent the transition from being executed if it
returns false. The effect
function (optional) will be executed
during the transitioning of the function. If no events are specified,
this is interpreted as any events will trigger the transition.
Three ways of specifying the src
and target
states are
supported: local, relative or absolute. In the above example
stateX
and stateY
are referenced locally and must therefore be
defined within the same composite state as the transition.
Relative references specify states that are more deeply nested (relative to the position of the transition). Such references starts with a leading dot. For example:
return rfsm.state{
operational=rfsm.state{
motors_on = rfsm.state{
moving = rfsm.state{},
stopped = rfsm.state{},
rfsm.trans{src='initial', tgt='stopped'},
},
rfsm.trans{src='initial', tgt='motors_on'},
},
off=rfsm.state{},
rfsm.trans{src='initial', tgt=".operational.motors_on.moving" }
rfsm.trans{src=".operational.motors_on.stopped", tgt='off', events={'e_off'} }
}
The first transition is defined between the (locally referenced)
initial
connector to the relatively referenced moving
state. This
permits to refine the default behavior of the operational state,
namely entering motors_on.stopped
(due to the initial connectors),
to instead enter the motors_on.moving
state.
The second transition defines a transition from the relatively
referenced operational.motors_on.stopped
to off
. Here the
intention is to constrain the states from which one can reach the
off
state: turning the device off is only permitted if it is not
moving.
At last absolute references begin with "root." Using absolute syntax is strongly discouraged for anything other than testing, as it breaks compositionality: if a state machine is used within a larger statemachine the absolute reference is broken.
Furthermore, transitions support so called priority
numbers. Priority numbers serve to resolve conflicts within one
hierarchical level. In case two transitions are enabled by a set of
events, the transition with the higher priority number will be
executed. Priority numbers are defined with the pn
keyword on
transitions, as shown below. Transitions without priority numbers are
assumed to have priority 0.
rfsm.trans{ src='following', tgt='hitting', pn=10, events={ 't6' } },
If possible, statecharts should be designed not to depend on priority numbers and introduce these rather as an optimization.
Connectors permit to define so called compound transitions by chaining multiple transition segments together. Connectors are similar to the UML junction element. Compound transitions are statically evaluated, meaning that the compound transition is only executed if each subtransition is enabled (events match and guards are true).
Also see the examples connector_simple.lua
and
connector_split.lua
.
Connectors are useful for defining interfaces (entry and exit points) that hide internals of a composite state. The following example defines a error handling state:
return rfsm.state{
software_err = rfsm.state{},
hardware_err = rfsm.state{},
initial = rfsm.conn{},
recovered = rfsm.conn{},
failed = rfsm.conn{},
rfsm.trans{src='initial', tgt='software_err', events={'e_sw_err'}},
rfsm.trans{src='initial', tgt='hardware_err', events={'e_hw_err'}},
rfsm.trans{src='software_err', tgt='recovered', events={'e_recovered'}},
rfsm.trans{src='hardware_err', tgt='recovered', events={'e_recovered'}},
rfsm.trans{src='software_err', tgt='failed', events={'e_failed'}},
rfsm.trans{src='hardware_err', tgt='failed', events={'e_failed'}},
}
Transitions 1 and 2 dispatch to different error handling states based on the events received. Transitions 3, 4, 5 and 6 connect the states to different exit connectors based on the events they generate.
Note: defining cycles is possible, but dangerous, unsupported and discouraged. It may make the yogurt in your fridge grow fine grey beards.
Before running a statemachine must be initalized. This serves to
validate the fsm model and transform the fsm to be suitable for
execution. Initalization is done using the rfsm.init(fsm)
function,
that takes a (string) rfsm description as input and returns an
initalized fsm. To load an rfsm from a file and initialize it, the
rfsm.load(filename)
function can be used:
fsm = rfsm.init(rfsm.load("fsm.lua"))
If the return value from rfsm.init
is not false
, initalization
succeeded and the returned fsm can be run.
The function rfsm.step(fsm, n)
will attempt to step the given fsm
for a maximum of n
times. A step can be either the execution of a
transition or a single execution of the doo
program. step
will
return either when the state machine is idle or the given number
of steps has been reached. The boolean return value indicates whether
the fsm is idle (true
) or the maximum amount of requested steps was
reached (false
).
For each step the rfsm engine will invoke the getevents
hook to
retrieve new events and then reason about what to do (which
transitions to execute or doo
's to run). After that these events are
discarded. If this seems inconvenient, checkout the event memory
extension.
When omitted, the number of steps argument n
to rfsm.step
defaults
to 1.
rfsm.run(fsm)
calls step
as long as the given fsm is not idle. Not
idle means: there are either events in the queue or there is an active
doo
function that is not idle.
To directly send events to the fsm the function rfsm.send_events(fsm, e1, e2, ...)
can be used. The first argument is the fsm to which all
subsequent event arguments are sent to.
-
Name clashes between state/connector names with reserved Lua keywords.
This can be worked around by using the following syntax:
['end'] = rfsm.state{...}
- Executing functions accidentially
It is a common mistake to execute externally defined functions instead of adding references to them:
stateX = rfsm.state{ entry = my_func() }
The (likely) mistake above is to execute my_func
and assigning the
result to entry instead of assigning my_func
:
stateX = rfsm.state{ entry = my_func }
Of course the first example would be perfectly valid if my_func()
returned a function as a result!
-
Why doesn't my statemachine react if I send a completion event
e_done
from the outside?Short answer: because it is a syntactic shortcut for the completion event of the source state of the transition which it is defined on. During initalization it is transformed to
e_done@fqn
(e.g.e_root@root.stateA.stateB
) If you send in the expanded completion event it will work.Explanation: a completion event only makes sense in the context of a state which completed. Making the state which has completed explicit in the event avoids accidentially triggering a transition labeled with a higher priority completion event that has nothing to do with the current one.
The same holds true for
rfsm_timeevent
based timeevents. -
My FSM is using up 100% CPU, what's wrong?
Most likely you have defined a long running
doo
function that does not callrfsm.yield
with atrue
argument (the idle flag). Therefore the rFSM engine calls thedoo
function in a tight loop. -
My FSM is doing nothing, my guard are not executed, … , although I'm running
step
orrun
periodically!rFSM will only attempt to transition if it has at least one event in the queue! If you only want to transition based on guards, raise a dummy event (e.g. "e_any").
This extension adds memory of events that occurred to an rFSM
statechart. This is done maintaining a table emem
for every
state. The keys in this table are event names and the values the
number of times that event occurred while the respective state was
active. The emem
table is cleared when a state is exited by setting
all values to 0.
This extension is useful for defining transitions that are taken only after certain events have occurred, but that do not necessarily occur within one step. Because the rFSM engine drops events after each steps this information would otherwise be lost.
To enable event memory, all you need to do is load the rfsm_emem
module. Checkout the examples/emem_test.lua
for more details.
In a nutshell, this plugin permits to trigger transitions only after multiple events have been received. These events can be received in different steps.
This is basically a specialized version of the emem plugin. This one should be preferred if no counting is required, since it is computationally much less expensive.
Behavior: When loaded, the plugin scans for events with the syntax await(event1, event2)". This statement is transformed as follows:
-
a guard condition is generated and added to possibly existing guard conditions. It will only enable the transition if the both events have been received while the source state is active.
-
a second hook is installed in the exit function of the source state to reset the event counting. So when the source state is exited (either via or not via the await transition) and reentered again, the counting start from the beginning. It would be trivial to provide a variant of await that resets the counts only if the await transition is taken, however it is not clear right now if that would be useful at all.
For more information checkout the await.lua
example.
This module extends the rFSM engine with time events. Time events are
automatically raised after the specified time after entering a state
has elapsed. To enable time events, it suffices to load the
rfsm_timeevent
module. Currently only relative (opposed to absolute)
timeevents are supported. These can be specified on transitions using
the e_after(duration)
syntax, as show in the following example:
rfsm.trans{ src='A', tgt='B', events={ 'e_after(0.1)' } },
The timeevent will be raised 100ms after state A
was entered.
The only requirement to use rfsm_timeevents is that a gettime
function is configured using the rfsm_timeevent.set_gettime_hook(f)
function. This function is expected to return the current time in two
return values: seconds, nanoseconds.
An example can be found in examples/timeevent.lua
Warning: these timeevents only work while the rfsm engine is running and can not magically wake up an idle fsm. Therefore this type of timeevents typically only makes sense for fsm that are "stepped" at a fixed frequency or that never go idle.
The rfsmpp.gen_dbgcolor
function generates a configurable and
colorful dbg
hook.
Usage:
rfsmpp.gen_dbgcolor(name, dbgids, defshow)
name
is the (optional) string name to print prefixing the debug outputdbgids
is a table that enables or disables certain dbg ids by setting them to true or false. Known debug ids are:STATE_ENTER
,STATE_EXIT
,EFFECT
,DOO
,EXEC_PATH
,ERROR
,HIBERNATING
,RAISED
,TIMEEVENT
defshow
(bool) defines whether debug id's not mentioned in the dbgids table are shown or not.
Example:
fsm = rfsm.init(...)
fsm.dbg=rfsmpp.gen_dbgcolor("fsm1",
{ STATE_ENTER=true, STATE_EXIT=true},
false)
Will show only STATE_ENTER
and STATE_EXIT
debug messages.
This debugging helper plugin will at load-time construct a list of all events used in the FSM. If at runtime an event is received which is not known in the known list, a warning message will be printed.
To use, just require the module before creating your fsm. Important: load it after other plugins that transform events (such as timeevents), so that it picks up the transformed events.
Modules to transform rFSM models to graphical descriptions. rfsm2uml
generates classical statechart figures and rfsm2tree
generates a
tree representation (useful to see check priorities).
Usage:
rfsm2uml.rfsm2uml(root_fsm, format, outfile, caption)
rfsm2tree.rfsm2tree(root_fsm, format, outfile)
Examples:
require("rfsm2uml")
fsm = rfsm.init(rfsm.load("fsm.lua"))
rfsm2uml.rfsm2uml(fsm, 'png', "fsm.png", "Figure caption")
or
require("rfsm2tree")
fsm = rfsm.init(rfsm.load("fsm.lua"))
rfsm2tree.rfsm2tree(fsm, 'png', "fsm-tree.png")
The rfsm-viz
command line uses these modules to generate pictures.
to generate all possible formats run:
$ tools/rfsm-viz all examples/composite_nested.lua
generates various representations (in examples/
)
small command line simulator for running a fsm interactively.
$ tools/rfsm-sim all examples/ball_tracker_scope.lua
It requires an image viewer which automatically updates once the file
displayed changes. For example evince
works nicely.
Based on rfsm2json.lua
module and requires lua-json.
See the Orocos LuaCookbook for more details.
The graphical model:
and the corresponding textual representation:
-- any rFSM is always contained in a state
return rfsm.state {
dbg = true, -- enable debugging
on = rfsm.state {
entry = function () print("disabling brakes") end,
exit = function () print("enabling brakes") end,
moving = rfsm.state {
entry=function () print("starting to move") end,
exit=function () print("stopping") end,
},
waiting = rfsm.state {},
-- define some transitions
rfsm.trans{ src='initial', tgt='waiting' },
rfsm.trans{ src='waiting', tgt='moving', events={ 'e_start' } },
rfsm.trans{ src='moving', tgt='waiting', events={ 'e_stop' } },
},
error = rfsm.state {
doo = function (fsm)
print ("Error detected - trying to fix")
rfsm.yield()
math.randomseed( os.time() )
rfsm.yield()
if math.random(0,100) < 40 then
print("unable to fix, raising e_fatal_error")
rfsm.send_events(fsm, "e_fatal_error")
else
print("repair succeeded!")
rfsm.send_events(fsm, "e_error_fixed")
end
end,
},
fatal_error = rfsm.state {},
rfsm.trans{ src='initial', tgt='on',
effect=function() print("initializing system") end },
rfsm.trans{ src='on', tgt='error', events={ 'e_error' } },
rfsm.trans{ src='error', tgt='on', events={ 'e_error_fixed' } },
rfsm.trans{ src='error', tgt='fatal_error', events={ 'e_fatal_error' } },
rfsm.trans{ src='fatal_error', tgt='initial', events={ 'e_reset' } },
}
This is easy! Let's assume the state machine is is a file "subfsm.lua"
and uses the strongly recommended return rfsm.state ...
syntax, it
can be included as follows:
return rfsm.state {
name_of_state = rfsm.load("subfsm.lua"),
otherstateX = rfsm.state{},
...
}
Make sure not to forget the ',' after the rfsm.load()
statement!
The LuaCookbook page describes how to do this.
Functions to define rFSM:
Function | Short alias | Description * |
---|---|---|
state{} |
state{} |
create a state |
connector{} |
conn{} |
create a connector |
transition{} |
trans{} |
create a transition |
Function | Description |
---|---|
fsm rfsm.init(fsmmodel) |
create an initialized rfsm instance from model |
idle rfsm.step(fsm, n) |
attempt to transition FSM n times. Default: once |
rfsm.run(fsm) |
run FSM until it goes idle |
rfsm.send_events(fsm, ...) |
send one or more events to internal rfsm event queue |
The following hook functions can be defined for a toplevel composite state and allow to refine various behavior of the state machine.
Function | Description |
---|---|
dbg |
called to output debug information. Set to false to disable. Default: false. |
info |
called to output informational messages. Set to false to disable. Default: stdout. |
warn |
called to output warnings. Set to false to disable. Default stderr. |
err |
called to output errors. Set to false to disable. Default stderr. |
table getevents() |
function which returns a table of new events which have occurred. |
Lower level functions (not for normal use):
Use these to manage step hooks. Setting pre_step_hook
and
post_step_hook
directly is not permitted anymore:
Function | Description |
---|---|
pre_step_hook_add(fsm, hook, where) |
install function hook to be called before each rfsm step of fsm |
post_step_hook_add(fsm, hook, where) |
install function hook to be called after each rfsm step of fsm |
idle_hook(fsm)
: if defined, called instead of returning from
step/run functions. Used only for debugging purposes.
1: See this Real-time Linux Workshop paper, lua-tlsf and the minimal Lua real-time POSIX bindings
2: The reason for this choice of default is that it fails more obviously (100% CPU load) than the opposite (doo function not executed properly).