LXSC stands for "Lua XML StateCharts", and is pronounced "Lexie". The LXSC library allows you to run SCXML state machines in Lua. The Data Model for interpretation is all evaluated Lua, allowing you to write conditionals and data expressions in one of the best scripting languages in the world for embedded integration.
local LXSC = require"lxsc-min-12"
local scxml = io.open('my.scxml'):read('*all')
local machine = LXSC:parse(scxml)
machine:start() -- initiate the interpreter and run until stable
machine:fireEvent("my.event") -- add events to the event queue to be processed
machine:fireEvent("another.event.name") -- as many as you like; they won't have any effect until you
machine:step() -- call step() to process all events and run until stable
print("Is the machine still running?",machine.running)
print("Is a state in the configuration?",machine:isActive('some-state-id'))
-- Keep firing events and calling step() to process them
The data model used by the interpreter is a Lua table. This table is used to store and retrieve the values created via <data>
or <assign>
. This table is also used as the environment under which the <script>
blocks run and the code="…"
attributes of <transition>
elements are evaluated.
Providing your own data model table allows you to:
- supply an initial set of data values—useful for initial conditional transitions
- expose functions, either utilities like
print()
or custom defined functions that provide the meat for a simple<script>doTheThing()</script>
semantic callbacks - create a custom datatable that performs metamagic when new keys are accessed or modified by the state machine
You supply a custom data model table by passing a named data
parameter to the start()
method:
local mydata = { reloading=true, userName="Gavin" } -- populate initial data values
local funcs = { print=print, doTheThing=utils.doIt } -- create 'global' functions
setmetatable( mydata, {__index=funcs} )
machine:start{ data=mydata }
There are six special keys that you may set to a function value on the machine to keep track of what the machine is doing: onBeforeExit
, onAfterEnter
, onEnteredAll
, onDataSet
, onTransition
, and onEventFired
.
machine.onBeforeExit = function(stateId,stateKind,isAtomic) ... end
machine.onAfterEnter = function(stateId,stateKind,isAtomic) ... end
machine.onEnteredAll = function() ... end
The state-specific change callbacks are passed three parameters:
- The string id of the state being exited or entered.
- The string kind of the state:
"state"
,"parallel"
, or"final"
.- The callbacks are not invoked for
history
orinitial
pseudo-states.
- The callbacks are not invoked for
- A boolean indicating whether the state is atomic or not.
As implied by the names the onBeforeExit
callback is invoked right before leaving a state, while the onAfterEnter
callback is invoked right after entering a state.
The onEnteredAll
callback will be invoked once after the last state is entered for a particular microstep.
machine.onDataSet = function(dataid,newvalue) ... end
If supplied, this callback will be invoked any time the data model is changed.
Warning: using this callback may slow down the interpreter appreciably, as many internal modifications take place during normal operation (most notably setting the _event
system variable).
machine.onTransition = function(transitionTable) ... end
The onTransition
callback is invoked right before the executable content of a transition (if any) is run.
Warning: the table supplied by this callback is an internal representation whose implementation is not guaranteed to remain unchanged. Currently you can access the following keys for information about the transition:
type
- the string"internal"
or"external"
.cond
- the string value of thecond="…"
attribute, if any, ornil
._event
- the string value of theevent="…"
attribute, if any, ornil
._target
- the string of thetarget="…"
attribute, if any, ornil
.events
- an array of internalLXSC.Event
tables, one for each event, ornil
.targets
- an array of internalLXSC.State
tables, one for each target, ornil
.- Any custom attributes supplied on the transition appear as direct attributes (with no namespace information or protection).
machine.onEventFired = function(eventTable) ... end
The onEventFired
callback is invoked whenever fireEvent()
is called on the machine (either by your own code or by internal machine code). The event has not been processed, and there is no guarantee that the event is going to cause any effect later on. This is mostly a debugging callback allowing you to ensure that events you thought that you were injecting were, in fact, making it in.
The table supplied to this callback is a LXSC.Event
object with the following keys:
-
name
- the string name of the fired event, e.g."foo"
or"foo.bar.jim.jam"
. -
data
- whatever data (if any) was supplied as the second parameter tofireEvent()
. -
triggersDescriptor
- a function that can be used to determine if this event would trigger a particular transition'sevent="…"
descriptor.machine.onEventFired = function(evt) print(evt.name, evt:triggersDescriptor('a'), evt:triggersDescriptor('a.b')) end machine:fireEvent("a") --> a true nil machine:fireEvent("a.b") --> a true true
-
triggersTransition
- similar totriggersDescriptor()
, but it takes a transition table (as supplied to theonTransition
callback) and uses the event descriptor(s) for that transition'sevent="..."
attribute to evaluate if the event should cause the transition to be triggered.- Note: this does not test any conditional code that may be present inthe transition's
cond="..."
attribute. This function may return true, and then the transition may subsequently not be triggered by this event if the conditions are not right.
- Note: this does not test any conditional code that may be present inthe transition's
-
_tokens
- an array of the event name split by periods (an implementation detail used for optimized transition descriptor matching).
Note that the event object described above is also returned from machine:fireEvent()
, in case you need that.
While the machine is running (after you have called start()
) you can peek at the data for a specific location via:
local theValue = machine:get("dataId")
…and you can set the value for a particular location via:
machine:set("dataId",someValue)
You can evaluate code in the data model (just like a cond="…"
or expr="…"
attribute does) by:
local theResult = machine:eval("mycodestring")
…and you can run arbitrary code against the data model (just like a <script>
block does) by:
machine:run("mycodestring")
You can ask a running machine if a particular state id is active (in the current configuration):
print("Is the foo-bar state active?", machine:isActive('foo-bar'))
…or you can ask for the set of all states that are active:
for stateId,_ in pairs(machine:activeStateIds()) do
print("This state is currently active:",stateId)
end
…or you can ask just for the set of atomic (no sub-state) states:
for stateId,_ in pairs(machine:activeAtomicIds()) do
print("This atomic state is currently active:",stateId)
end
You can also ask for a list of all state IDs in the machine, including those autogenerated for states that have no id="…"
attribute:
for stateId,_ in pairs(machine:allStateIds()) do
print("One of the states has this id:",stateId)
end
You can ask a machine for the set of all events that trigger transitions:
for eventDescriptor,_ in pairs(machine:allEvents()) do
-- eventDescriptor is a simple dotted string, e.g. "foo.bar"
print("There's at least one transition triggered by:",eventDescriptor)
end
…or you can ask just for the events that may trigger a transition in the current configuration:
for eventDescriptor,_ in pairs(machine:availableEvents()) do
print("There's at least one active transition triggered by:",eventDescriptor)
end
If your state machine uses delayed events (<send event="e" delayexpr="'200ms'"/>
) LXSC defaults to using Lua's os.clock()
to measure elapsed time. To use your own timer, override the elapsed
method on either the SCXML object, or your own machine instance:
-- Overriding for every LXSC instance
local LXSC = require'lxsc-min-14'
LXSC.SCXML.elapsed = mytimer()
-- Overriding for a specific machine
local machine = LXSC:parse(myscxml)
function machine:elapsed() return mytimer() end
Anywhere that executable content is permitted—in <onentry>
, <onexit>
, and <transition>
—a state chart may specify custom elements via a custom XML namespace. For example:
<state xmlns:my="goodstuff">
<onentry><my:explode amount="10"/></onentry>
</state>
With no modifications, when LXSC encounters such an executable it fires an error.execution.unhandled
event internally with the _event.data
set to the string "unhandled executable type explode"
.
Internal error events do not halt execution of the intepreter (unless the state machine reacts to that event in a violent manner, such as transitioning to a <final>
state). However, if you want such elements to actually do something, you must extend LXSC to handle the executable type like so:
local LXSC = require'lxsc-min-12'
function LXSC.Exec:explode(machine)
print("The state machine wants to explode with an amount of",self.amount)
return true
end
The current machine is passed to your function so that you may call :fireEvent()
, :eval()
, etc. as needed. Attributes on the element are set as named keys on the self
table supplied to your function (e.g. amount
above).
Your handler must return true
if it is successful, and return nil
or false
if something prevents it from executing correctly.
Note: executable elements with conflicting names in different namespaces will use the same callback function. The only way to disambiguate them currently is via a _nsURI
property set on the table. For example, to handle this document:
<state xmlns:my="goodstuff" xmlns:their="badstuff">
<onentry>
<my:explode amount="10"/>
<their:explode chunkiness="very"/>
</onentry>
</state>
you would need to do something like:
function LXSC.Exec:explode(machine)
if self._nsURI=='goodstuff' then
print("The state machine wants to explode with an amount of",self.amount)
else
machine:fireEvent(
"error.execution.unhandled",
"Dunno how to handle 'explode' in the "..self._nsURI.." namespace"
)
end
end
You can also use this to re-implement or augment existing executables like <log>
:
-- Augmenting the <log> to use a logger with a custom logging level, e.g.
-- <transition event="error.*">
-- <log label="An error occurred" expr="_event.data" my:log-level="error" />
-- </transition>
function LXSC.Exec:log(machine)
local result = {self.label}
if self.expr then table.insert(result,machine:eval(self.expr)) end
local level = self['log-level'] or 'info'
my_global_logger[level]( my_global_logger, table.concat(result,": ") )
return true
end
LXSC aims to be almost 100% compliant with the SCXML Interpretation Algorithm. However, there are a couple of minor variations (compared to the Working Draft as of 2013-Feb-14):
- Manual Event Processing: Where the W3C implementation calls for the interpreter to run in a separate thread with a blocking queue feeding in the events, LXSC is designed to be frame-based. You feed events into the machine and then manually call
my_lxsc:step()
to crank the machine in the same thread. This will cause the event queues to be fully processed and the machine to run until it is stable, and then return. Rinse/repeat the process of event population followed by callingstep()
each frame.- This single-threaded, on-demand approach affects a delayed
<send>
the most. While a<send event="e" delay="1s"/>
command will not inject the event at least one second has passed, it could be substantially longer than that if your script only callsstep()
every 30 seconds, or (worse) waits until some user interaction occurs to callstep()
again.
- This single-threaded, on-demand approach affects a delayed
- Configuration Clearing: The W3C algorithm calls for the state machine configuration to be cleared when the interpreter is exited. LXSC will instead leave the configuration (and data model) intact for you to inspect the final state of the machine.
- No
<invoke>
or IO Processors: Currently LXSC does not implement<invoke>
,<send>
targeting another entity, or any IO Processors. These features may be added in the future.- You can see the current result of the W3C test suite in the file
test/scxml-suite/scxml10-ir-results-lxsc.xml
.
- You can see the current result of the W3C test suite in the file
<assign>
elements do not support executable child content instead ofexpr="…"
.<send>
selements do not support thetype
/typeexpr
/target
/targetexpr
attributes other than in trivial (same-session) cases.- No support for inter-machine communication.
- No support for
<invoke>
. - Data model locations like
foo.bar
get and set a single key instead of nested tables.
LXSC is copyright ©2013-2014 by Gavin Kistner and is licensed under the MIT License. See the LICENSE.txt file for more details.
For bugs or feature requests please open issues on GitHub. For other communication you can email the author directly.