Simple things should be simple, complex things should be possible.
—Alan Kay
Recipes#
These sections contain examples that can be referenced as you are building up your own statechart programs.
Demonstration of Capabilities#
In this section I’ll show you how you can use the miros library by layering more and more of its features into a simple program. This program will be arbitrary, it serves no purpose other than to show how to do some common things.
State Abstraction in Miros#
In miros a state is a behavioral specification for an event processor. A state is a function that does the following:
It accepts two arguments:
an object of type/subclass of
miros.ActiveObject
, which has an event processor.an event of type/subclass of
miros.Event
It describes how it is situated in a hierarchy and how it is connected to other states, by changing the
temp.fun
attribute of its first argument.It returns an
miros.return_status
attribute, which tells the event processor how the state function reacted to the event.
The user starts and then posts events to the ActiveObject, and its event processor reacts to these events by calling your state functions over and over again with a set of internal and external events. The internal events are used to search the hierarchical state machine, to run its entry and exit conditions and to initialize the state once it has been settled into. The external events are user defined (more will be said about this shortly).
The state functions do two different things, they describe how they are topologically related to other states and they contain code which will run as the event processor calls them over and over again. What emerges from this interplay is a Hierarchical State Machine (HSM) behavior which follows the Harel Formalism (picture-to-behavior rules).
Now that we understand a bit of theory and how the miros abstraction works, let’s write some code.
Minimal Viable States#
In a statechart diagram, a state is represented by a named rounded rectangle. In a hierarchical state machine a state can exist within a state. Here we see an inner_state placed within an outer_state:
Every state function, must at least describe where it is situated in the HSM hierarchy. Here is the miros code for the above diagram:
def outer_state(chart, e):
chart.temp.fun = chart.top # describe how we fit in the hierarchy
status = return_status.SUPER # describe how we reacted to the event
return status
def inner_state(chart, e):
chart.temp.fun = outer_state # describe how we fit in the hierarchy
status = return_status.SUPER # describe how we reacted to the event
return status
Above we see two minimal-viable state functions, which react to every possible
event the same way. They set the temp.fun
to their super state (its outer
state) and return a status telling the event processor that they did this.
Now that we know how to make a minimal-viable state function, and how it relates to a hierarchical state machine, let’s talk about how to connect it to a thread, how to start it and how to give it some behavior.
Attachment Points and a Working State Machine#
A state function is lazy about how it describes its graphical relationship to
the other states in its hierarchical state machine. To ask it a question about
how it sits in the hierarchy, or how it is connected to another part of the
state machine, the event processor needs to send it an event. The state
function will respond by setting its return status to something and it will
change the temp.fun
attribute of its first argument.
But before it can do either of these things, an event processor needs to be
connected to a state function. This is done with the Active object’s start_at
method. On the diagram, this is called the attachment point. It is where the
ball and the socket meet.
I’ll redraw our simple state machine with some more details:
The above diagram is saying that there is an attachment point between an
ActiveObject
and the outer_state
. To create and run a state machine, we
will instantiate the ActiveObject
, then start_at
the outer_state
.
When we call this start_at
method, the miros library will create a thread
for this statechart and run it until the main program is stopped.
We have also added a new graphing element called the init pseudostate, the black
dot. The black dot has an arrow pointing to the inner_state
. This means,
after I have entered into the outer_state
and I have settled, transition
into the inner_state
.
To make our new design work, we will have to change our outer_state
function. It will need to provide graphical information about an init event.
It will set the temp.fun
to inner_state
and it will have to tell the
event processor it needs to perform a transition into another state. This is
all done within the trans
method:
import time
from miros import signals
from miros import ActiveObject
from miros import return_status
def outer_state(chart, e):
status = return_status.UNHANDLED
# the event processor is asking us about events called INIT_SIGNAL
if(e.signal == signals.INIT_SIGNAL):
# we are transitioning to inner_state
# we let the trans method, set temp.fun and our return status
status = chart.trans(inner_state)
# we do this for any other event
else:
chart.temp.fun = chart.top
status = return_status.SUPER
return status
def inner_state(chart, e):
# we do this for all events
chart.temp.fun = outer_state
status = return_status.SUPER
return status
if __name__ == '__main__':
ao = ActiveObject('ao')
# Create a thread and start our state machine
ao.start_at(outer_state)
# Run our main program so that the state machine's thread
# can do some stuff.
# The state machine's thread will be stopped when our main thread stops
time.sleep(0.01)
I have highlighted the code that would cause the init transition and the
attachment point (start_at
). If we run this code, the statechart will start
in the outer_state
, then settle, then transition into the inner_state
.
But you will have to trust me about this, since the code doesn’t provide any
user feedback.
Now that we know how to build and start a small statechart. Let’s look at how to instrument it so it provides feedback about its behavior.
Feedback about Behavior through Instrumentation#
We will include the spy_on
decorator from the miros library, then we will
use it to decorate our state functions:
import time
from miros import spy_on
from miros import signals
from miros import ActiveObject
from miros import return_status
@spy_on # enables the live_trace/live_spy capabilities
def outer_state(chart, e):
status = return_status.UNHANDLED
# the event processor is asking us about events called INIT_SIGNAL
if(e.signal == signals.INIT_SIGNAL):
# we are transitioning to inner_state
# we let the trans method, set temp.fun and our return status
status = chart.trans(inner_state)
# we do this for any other event
else:
chart.temp.fun = chart.top
status = return_status.SUPER
return status
@spy_on # enables the live_trace/live_spy capabilities
def inner_state(chart, e):
# we do this for all events
chart.temp.fun = outer_state
status = return_status.SUPER
return status
if __name__ == '__main__':
ao = ActiveObject('ao')
ao.live_trace = True # so we can see what is happening
# Create a thread and start our state machine
ao.start_at(outer_state)
# Run our main program so that the state machine's thread
# can do some stuff.
# The state machine's thread will be stopped when our main thread stops
time.sleep(0.01)
The @spy_on
decorators enables the live_trace and live_spy instrumentation
capabilities of the statechart library.
So if I run the above code I will see something like this in my terminal:
[2019-07-22 12:22:34.050461] [ao] e->start_at() top->inner_state
Now that we know how to instrument a statechart, let’s look at how to add some state entry conditions.
Entry Conditions and Handled Events#
We will add some entry code to both the outer_state and inner_state functions:
The above diagram is saying, when we enter the outer_state
, run a print
statement. Then settle, which will cause an init transition into the
inner_state
. When we enter the inner_state
run a different print
statement. If a state function receives an entry event, we don’t want to change
our active state and we don’t want to describe our super state. We just want to
run some code, then tell the event processor that the event was handled so it
will stop trying to figure out what to do with it.
Here is the code:
# simple_state_3.py
import time
from miros import spy_on
from miros import signals
from miros import ActiveObject
from miros import return_status
@spy_on
def outer_state(chart, e):
status = return_status.UNHANDLED
# the event process automatically sends
# an event named ENTRY_SIGNAL when a state is entered
if(e.signal == signals.ENTRY_SIGNAL):
print("hello from outer_state")
status = return_status.HANDLED
elif(e.signal == signals.INIT_SIGNAL):
print("init")
status = chart.trans(inner_state)
else:
chart.temp.fun = chart.top
status = return_status.SUPER
return status
@spy_on
def inner_state(chart, e):
status = return_status.UNHANDLED
# the event process automatically sends
# an event named ENTRY_SIGNAL when a state is entered
if(e.signal == signals.ENTRY_SIGNAL):
print("hello from inner_state")
status = return_status.HANDLED
else:
chart.temp.fun = outer_state
status = return_status.SUPER
return status
if __name__ == '__main__':
ao = ActiveObject('ao')
ao.live_trace = True
ao.start_at(outer_state)
time.sleep(0.01)
If we run the code we see something like this:
hello from outer_state
init
hello from inner_state
[2019-07-22 12:22:34.050461] [ao] e->start_at() top->inner_state
So far we have been talking about signals that are included within the miros library: INIT_SIGNAL, ENTRY_SIGNAL.
Let’s adjust our design to use another internal signal, EXIT_SIGNAL.
Note
If you use miros to build active objects, you are making a multithreaded
system. Python’s print
function is not thread safe, which means if two or
more of your active objects try to print something at the exact same time, a
run time error could occur.
As of miros 4.2.1
miros provides a thread safe print
function. To
access it in this example you could write self.print("hello from
inner_state")
instead of print("hello from inner_state")
.
Exit Conditions#
Our new design describes some code that will run when we exit either state. But how would we ever exit? There is nothing on our diagram that can cause an exit, we can only climb into the inner_state, then sit their forever.
To add some more behavior, we will have to invent a signal.
Inventing your own Signals: External events#
Let’s invent a signal and call it Reset
, because it will reset the chart, or
put it back into the state it was when we first started it at the
outer_state
. Any signal that is not an internal signal, like INIT_SIGNAL,
ENTRY_SIGNAL, EXIT_SIGNAL.. is called an external signal. In this case our
external signal Reset
will be invented the moment we write it into the code;
it is automatically declared.
So, how do we invent an event and give it a signal name and send it at our chart? Well, the chart runs in a thread and it has a set of queues that it watches for events. To send this chart information we just have to make an event, assign it to a signal, then post it into one of these queues. Here is the code that will do this:
# simple_state_5.py
import time
from collections import namedtuple
from miros import Event
from miros import spy_on
from miros import signals
from miros import ActiveObject
from miros import return_status
@spy_on
def outer_state(chart, e):
status = return_status.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
print("hello from outer_state")
status = return_status.HANDLED
elif(e.signal == signals.INIT_SIGNAL):
print("init")
status = chart.trans(inner_state)
elif(e.signal == signals.Reset):
print("resetting the chart")
status = chart.trans(outer_state)
elif(e.signal == signals.EXIT_SIGNAL):
print("exiting outer_state")
status = return_status.HANDLED
else:
chart.temp.fun = chart.top
status = return_status.SUPER
return status
@spy_on
def inner_state(chart, e):
status = return_status.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
print("hello from inner_state")
status = return_status.HANDLED
elif(e.signal == signals.EXIT_SIGNAL):
print("exiting inner_state")
status = return_status.HANDLED
else:
chart.temp.fun = outer_state
status = return_status.SUPER
return status
if __name__ == '__main__':
ao = ActiveObject("ao")
ao.live_trace = True
ao.start_at(outer_state)
ao.post_fifo(Event(signal=signals.Reset))
# let the thread catch up before we exit main
time.sleep(0.01)
If we run this code we see the following:
hello from outer_state
init
hello from inner_state
[2019-07-22 12:44:29.470827] [ao] e->start_at() top->inner_state
resetting the chart
exiting inner_state
exiting outer_state
hello from outer_state
init
hello from inner_state
[2019-07-22 12:44:29.471806] [ao] e->Reset() inner_state->inner_state
It behaves in a sensible way. But there is something interesting going on. The
state machine was in the inner_state
when it got our Reset
signal. It
ran the print statement associated with this signal, while it was still in the
inner_state
before it climbed out of the outer_state
and the
inner_state
, only to climb back into where it was.
Note
Another way to think about internal and external signals is that internal signals are sent from the event processor to the state functions without the user explicitly asking it to do so. But, external signals are only sent to the chart when a user explicitly posts the event into the statechart.
So the event processor needed to follow some rules. It needed to figure out how
and when to post each event to our two simple state functions. It needed to
figure out, who was the super state of inner_state
so it could call its
state function with the Reset
event to see what it should do.
The event processor does this work, and it keeps you from having to write code
to solve these types of topological problems. You just need to write simple
state functions, which act as a behavioral specification and then connect this
to an ActiveObject
, start it, and then post events to it.
Note
The code that is on the init arrow in our diagram and the code that is listed under entry and exit signals is run while the event processor is trying to figure out what to do next. The event processor is not aware of this code, the code is run as a side-effect of its efforts to search the graph then act upon its results.
What would have happened had our Reset
elif
clause just returned
return_status.HANDLED
? There wouldn’t have been a state transition.
When an event is caught this way it is called a hook.
Hooks#
Hook code can be run when you sent an event to the state or any of its substates which has the name of that hook. You are using the search feature of the event processor to do work for you, without asking it to change the state of your statechart as it reacts to your hook event. This is easier to understand with a picture.
Here is the code, with the hook highlighted
# simple_state_6.py
import time
from collections import namedtuple
from miros import Event
from miros import spy_on
from miros import signals
from miros import ActiveObject
from miros import return_status
@spy_on
def outer_state(chart, e):
status = return_status.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
print("hello from outer_state")
status = return_status.HANDLED
elif(e.signal == signals.INIT_SIGNAL):
print("init")
status = chart.trans(inner_state)
elif(e.signal == signals.Hook):
print("run some code, but don't transition")
status = return_status.HANDLED
elif(e.signal == signals.Reset):
print("resetting the chart")
status = chart.trans(outer_state)
elif(e.signal == signals.EXIT_SIGNAL):
print("exiting outer_state")
status = return_status.HANDLED
else:
chart.temp.fun = chart.top
status = return_status.SUPER
return status
@spy_on
def inner_state(chart, e):
status = return_status.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
print("hello from inner_state")
status = return_status.HANDLED
elif(e.signal == signals.EXIT_SIGNAL):
print("exiting inner_state")
status = return_status.HANDLED
else:
chart.temp.fun = outer_state
status = return_status.SUPER
return status
if __name__ == '__main__':
ao = ActiveObject("ao")
ao.live_trace = True
ao.start_at(outer_state)
ao.post_fifo(Event(signal=signals.Hook))
ao.post_fifo(Event(signal=signals.Reset))
# let the thread catch up before we exit main
time.sleep(0.01)
If you run this code you will see something like this:
hello from outer_state
init
hello from inner_state
[2019-07-22 13:13:59.860092] [ao] e->start_at() top->inner_state
run some code, but don't transition
resetting the chart
exiting inner_state
exiting outer_state
hello from outer_state
init
hello from inner_state
[2019-07-22 13:13:59.861465] [ao] e->Reset() inner_state->inner_state
We see that the “run some code, but don't transition
” print output occurred
between the start_at
call and the posting of the Reset
event. But there
is no mention of this hook in the time-stamped trace output. This is because
the trace instrumentation only presents high level state transition information.
The trace intentionally hides details.
Print statements are useful if you want to see if something is working; but you don’t always want them cluttering up your code.
Comprehensive Instrumentation with the live_spy and scribble#
The miros library provides a second kind of instrumentation when you wrap your
state functions inside of the @spy_on
decorators. You can turn on the
live_spy
instrumentation to spy on everything your event processor is
doing. Instead of printing, you can inject your messages into this spy stream,
by using the scribble
call. Let’s change our design a bit to demonstrate
these features:
Here is the code:
# simple_state_7.py
import time
from collections import namedtuple
from miros import Event
from miros import spy_on
from miros import signals
from miros import ActiveObject
from miros import return_status
@spy_on
def outer_state(chart, e):
status = return_status.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
chart.scribble("hello from outer_state")
status = return_status.HANDLED
elif(e.signal == signals.INIT_SIGNAL):
chart.scribble("init")
status = chart.trans(inner_state)
elif(e.signal == signals.Hook):
chart.scribble("run some code, but don't transition")
status = return_status.HANDLED
elif(e.signal == signals.Reset):
chart.scribble("resetting the chart")
status = chart.trans(outer_state)
elif(e.signal == signals.EXIT_SIGNAL):
chart.scribble("exiting outer_state")
status = return_status.HANDLED
else:
chart.temp.fun = chart.top
status = return_status.SUPER
return status
@spy_on
def inner_state(chart, e):
status = return_status.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
chart.scribble("hello from inner_state")
status = return_status.HANDLED
elif(e.signal == signals.EXIT_SIGNAL):
chart.scribble("exiting inner_state")
status = return_status.HANDLED
else:
chart.temp.fun = outer_state
status = return_status.SUPER
return status
if __name__ == '__main__':
ao = ActiveObject("ao")
ao.live_spy = True
ao.start_at(outer_state)
ao.post_fifo(Event(signal=signals.Hook))
ao.post_fifo(Event(signal=signals.Reset))
# let the thread catch up before we exit main
time.sleep(0.01)
If we run it we will see (I have highlighted the scribble statements):
START
SEARCH_FOR_SUPER_SIGNAL:outer_state
ENTRY_SIGNAL:outer_state
hello from outer_state
INIT_SIGNAL:outer_state
init
SEARCH_FOR_SUPER_SIGNAL:inner_state
ENTRY_SIGNAL:inner_state
hello from inner_state
INIT_SIGNAL:inner_state
<- Queued:(0) Deferred:(0)
Hook:inner_state
Hook:outer_state
run some code, but don't transition
Hook:outer_state:HOOK
<- Queued:(1) Deferred:(0)
Reset:inner_state
Reset:outer_state
resetting the chart
EXIT_SIGNAL:inner_state
exiting inner_state
SEARCH_FOR_SUPER_SIGNAL:inner_state
EXIT_SIGNAL:outer_state
exiting outer_state
ENTRY_SIGNAL:outer_state
hello from outer_state
INIT_SIGNAL:outer_state
init
SEARCH_FOR_SUPER_SIGNAL:inner_state
ENTRY_SIGNAL:inner_state
hello from inner_state
INIT_SIGNAL:inner_state
<- Queued:(0) Deferred:(0)
You can interleave the trace information into the spy information
by turning on both the live_spy
and the live_trace
. This would result in
this output:
[2019-07-26 06:29:33.774768] [ao] e->start_at() top->inner_state
START
SEARCH_FOR_SUPER_SIGNAL:outer_state
ENTRY_SIGNAL:outer_state
hello from outer_state
INIT_SIGNAL:outer_state
init
SEARCH_FOR_SUPER_SIGNAL:inner_state
ENTRY_SIGNAL:inner_state
hello from inner_state
INIT_SIGNAL:inner_state
<- Queued:(0) Deferred:(0)
Hook:inner_state
Hook:outer_state
run some code, but don't transition
Hook:outer_state:HOOK
<- Queued:(1) Deferred:(0)
[2019-07-26 06:29:33.776462] [ao] e->Reset() inner_state->inner_state
Reset:inner_state
Reset:outer_state
resetting the chart
EXIT_SIGNAL:inner_state
exiting inner_state
SEARCH_FOR_SUPER_SIGNAL:inner_state
EXIT_SIGNAL:outer_state
exiting outer_state
ENTRY_SIGNAL:outer_state
hello from outer_state
INIT_SIGNAL:outer_state
init
SEARCH_FOR_SUPER_SIGNAL:inner_state
ENTRY_SIGNAL:inner_state
hello from inner_state
INIT_SIGNAL:inner_state
<- Queued:(0) Deferred:(0)
This kind of feedback will become more and more important to you as you build more and more complex systems.
The attachment point between the ActiveObject
and the HSM in the diagram
serves double duty. It shows that there is an event processor that is using
these two state functions and it shows where the HSM is started. An
ActiveObject
has its own thread and all of the state machines memory is held
within it. The functions merely describe how and when different memory
operations should be performed, but the functions do not have their own memory.
For this reason, you can attach more than one ActiveObject
to the same HSM.
Attaching More than one ActiveObject to an HSM#
Here we see two different ActiveObject
objects attached to the same HSM.
Note
Technically speaking, the above UML diagram is incorrect. A class should not be drawn on a UML diagram more than once. I’m drawing it twice to make a point.
Note
A reminder that miros runs in python3.5 and above, so I am not using fstrings, since they were not included in python3.5
Here is the code:
# simple_state_8.py
import time
from collections import namedtuple
from miros import Event
from miros import spy_on
from miros import signals
from miros import ActiveObject
from miros import return_status
@spy_on
def outer_state(chart, e):
status = return_status.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
print("{}: hello from outer_state".format(chart.name))
status = return_status.HANDLED
elif(e.signal == signals.INIT_SIGNAL):
print("{}: init".format(chart.name))
status = chart.trans(inner_state)
elif(e.signal == signals.Hook):
print("{}: run some code, but don't transition".format(chart.name))
status = return_status.HANDLED
elif(e.signal == signals.Reset):
print("{}: resetting the chart".format(chart.name))
status = chart.trans(outer_state)
elif(e.signal == signals.EXIT_SIGNAL):
print("{}: exiting outer_state".format(chart.name))
status = return_status.HANDLED
else:
chart.temp.fun = chart.top
status = return_status.SUPER
return status
@spy_on
def inner_state(chart, e):
status = return_status.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
print("{}: hello from inner_state".format(chart.name))
status = return_status.HANDLED
elif(e.signal == signals.EXIT_SIGNAL):
print("{}: exiting inner_state".format(chart.name))
status = return_status.HANDLED
else:
chart.temp.fun = outer_state
status = return_status.SUPER
return status
if __name__ == '__main__':
ao1 = ActiveObject("ao1")
ao1.live_trace = True
ao1.start_at(outer_state)
ao1.post_fifo(Event(signal=signals.Hook))
ao1.post_fifo(Event(signal=signals.Reset))
ao2 = ActiveObject("ao2")
ao2.live_trace = True
ao2.start_at(inner_state)
ao2.post_fifo(Event(signal=signals.Hook))
ao2.post_fifo(Event(signal=signals.Reset))
# let the thread catch up before we exit main
time.sleep(0.01)
We have placed our print statements back into the state functions and we have
created two different ActiveObjects
starting the first in the outer_state
and starting the second in the inner_state. Then we send the Hook
and
Reset
events to both active objects.
Since each ActiveObject
runs in its own thread they will process their
events independent of each other; here is the output of this little program:
ao1: hello from outer_state
ao1: init
ao1: hello from inner_state
[2019-07-26 07:41:53.077229] [ao1] e->start_at() top->inner_state
ao2: hello from outer_state
ao2: hello from inner_state
ao1: run some code, but don't transition
[2019-07-26 07:41:53.080259] [ao2] e->start_at() top->inner_state
ao1: resetting the chart
ao1: exiting inner_state
ao1: exiting outer_state
ao1: hello from outer_state
ao1: init
ao1: hello from inner_state
[2019-07-26 07:41:53.080885] [ao1] e->Reset() inner_state->inner_state
ao2: run some code, but don't transition
ao2: resetting the chart
ao2: exiting inner_state
ao2: exiting outer_state
ao2: hello from outer_state
ao2: init
ao2: hello from inner_state
[2019-07-26 07:41:53.082678] [ao2] e->Reset() inner_state->inner_state
We can see that our output is kind of messy. The print messages are interleaved
because we have two ActiveObjects running in parallel, each in their own
thread. It turns out that the print
function is not thread safe, but the
Python logger is.
Making the Live Instrumentation use the Python Logger#
Let’s adjust our design a bit so that our live_trace and our live_spy will write to the Python logger instead of the terminal’s output.
We subclass the Activeobject
into a class which “has a” logger. This class will
have two custom instrumentation callbacks, one for our trace and one for our
spy. We will instantiate the class twice, and start one of the statecharts in
the outer_state
and the other in the inner_state
. Then we will send the
Hook
and Reset
events to both statecharts.
Here is the code:
# simple_state_9.py
import re
import time
import logging
from functools import partial
from miros import Event
from miros import spy_on
from miros import signals
from miros import ActiveObject
from miros import return_status
@spy_on
def outer_state(chart, e):
status = return_status.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
chart.scribble("hello from outer_state")
status = return_status.HANDLED
elif(e.signal == signals.INIT_SIGNAL):
chart.scribble("init")
status = chart.trans(inner_state)
elif(e.signal == signals.Hook):
chart.scribble("run some code, but don't transition")
status = return_status.HANDLED
elif(e.signal == signals.Reset):
chart.scribble("resetting the chart")
status = chart.trans(outer_state)
elif(e.signal == signals.EXIT_SIGNAL):
chart.scribble("exiting outer_state")
status = return_status.HANDLED
else:
chart.temp.fun = chart.top
status = return_status.SUPER
return status
@spy_on
def inner_state(chart, e):
status = return_status.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
chart.scribble("hello from inner_state")
status = return_status.HANDLED
elif(e.signal == signals.EXIT_SIGNAL):
chart.scribble("exiting inner_state")
status = return_status.HANDLED
else:
chart.temp.fun = outer_state
status = return_status.SUPER
return status
class ActiveObjectInstrumentToLog(ActiveObject):
def __init__(self, name, filename=None):
super().__init__(name)
if filename is None:
filename = 'simple_state_9.log'
logging.basicConfig(
format='%(asctime)s %(levelname)s:%(message)s',
filename=filename,
level=logging.DEBUG)
# ActiveObject has a register_live_trace_callback and a
# register_live_spy_callback interface, which can be used to
# change the live_trace and live_spy behavior. To use these
# registration methods, you write a function which accepts a
# string argument, provide this function as the input argument
# to the registration method and your custom function will
# stored, and then called each time a trace/spy string is
# generated from within the ActiveObject's instrumentation
# functions. By providing your own functions, you can log
# trace/spy information, or send it out over the network or do
# whatever you like with it.
# The register functions do not accept methods, they only accept
# functions that take a single argument. So we use the
# functool.partial to create a function with the self baked into
# it before it is passed into the register function. This way
# when miros calls this function with a string, we do not get a
# runtime error resulting from sending our customer function it
# too few arguments.
self.register_live_spy_callback(partial(self.spy_callback))
self.register_live_trace_callback(partial(self.trace_callback))
def trace_callback(self, trace):
'''trace without datetime-stamp'''
trace_without_datetime = re.search(r'(\[.+\]) (\[.+\].+)', trace).group(2)
logging.debug("T: " + trace_without_datetime)
def spy_callback(self, spy):
'''spy with machine name pre-pended'''
logging.debug("S: [{}] {}".format(self.name, spy))
if __name__ == '__main__':
ao1 = ActiveObjectInstrumentToLog("ao1")
ao1.live_trace = True
ao1.live_spy = True
ao2 = ActiveObjectInstrumentToLog("ao2")
ao2.live_trace = True
ao2.live_spy = True
ao1.start_at(outer_state)
ao1.post_fifo(Event(signal=signals.Hook))
ao1.post_fifo(Event(signal=signals.Reset))
ao2.start_at(inner_state)
ao2.post_fifo(Event(signal=signals.Hook))
ao2.post_fifo(Event(signal=signals.Reset))
# let the threads catch up before we exit main
time.sleep(0.01)
The log file created by this program would look something like this:
2019-07-26 11:37:22,674 DEBUG:T: [ao1] e->start_at() top->inner_state
2019-07-26 11:37:22,675 DEBUG:S: [ao1] START
2019-07-26 11:37:22,675 DEBUG:S: [ao1] SEARCH_FOR_SUPER_SIGNAL:outer_state
2019-07-26 11:37:22,675 DEBUG:S: [ao1] ENTRY_SIGNAL:outer_state
2019-07-26 11:37:22,675 DEBUG:S: [ao1] hello from outer_state
2019-07-26 11:37:22,675 DEBUG:S: [ao1] INIT_SIGNAL:outer_state
2019-07-26 11:37:22,675 DEBUG:S: [ao1] init
2019-07-26 11:37:22,676 DEBUG:S: [ao1] SEARCH_FOR_SUPER_SIGNAL:inner_state
2019-07-26 11:37:22,676 DEBUG:S: [ao1] ENTRY_SIGNAL:inner_state
2019-07-26 11:37:22,676 DEBUG:S: [ao1] hello from inner_state
2019-07-26 11:37:22,676 DEBUG:S: [ao1] INIT_SIGNAL:inner_state
2019-07-26 11:37:22,676 DEBUG:S: [ao1] <- Queued:(0) Deferred:(0)
2019-07-26 11:37:22,678 DEBUG:T: [ao2] e->start_at() top->inner_state
2019-07-26 11:37:22,678 DEBUG:S: [ao2] START
2019-07-26 11:37:22,678 DEBUG:S: [ao2] SEARCH_FOR_SUPER_SIGNAL:inner_state
2019-07-26 11:37:22,679 DEBUG:S: [ao2] SEARCH_FOR_SUPER_SIGNAL:outer_state
2019-07-26 11:37:22,679 DEBUG:S: [ao2] ENTRY_SIGNAL:outer_state
2019-07-26 11:37:22,679 DEBUG:S: [ao2] hello from outer_state
2019-07-26 11:37:22,679 DEBUG:S: [ao2] ENTRY_SIGNAL:inner_state
2019-07-26 11:37:22,679 DEBUG:S: [ao2] hello from inner_state
2019-07-26 11:37:22,679 DEBUG:S: [ao2] INIT_SIGNAL:inner_state
2019-07-26 11:37:22,680 DEBUG:S: [ao1] Hook:inner_state
2019-07-26 11:37:22,680 DEBUG:S: [ao2] <- Queued:(0) Deferred:(0)
2019-07-26 11:37:22,680 DEBUG:S: [ao1] Hook:outer_state
2019-07-26 11:37:22,681 DEBUG:S: [ao1] run some code, but don't transition
2019-07-26 11:37:22,681 DEBUG:S: [ao1] Hook:outer_state:HOOK
2019-07-26 11:37:22,681 DEBUG:S: [ao1] <- Queued:(1) Deferred:(0)
2019-07-26 11:37:22,682 DEBUG:T: [ao1] e->Reset() inner_state->inner_state
2019-07-26 11:37:22,682 DEBUG:S: [ao1] Reset:inner_state
2019-07-26 11:37:22,682 DEBUG:S: [ao1] Reset:outer_state
2019-07-26 11:37:22,682 DEBUG:S: [ao1] resetting the chart
2019-07-26 11:37:22,683 DEBUG:S: [ao1] EXIT_SIGNAL:inner_state
2019-07-26 11:37:22,683 DEBUG:S: [ao1] exiting inner_state
2019-07-26 11:37:22,683 DEBUG:S: [ao1] SEARCH_FOR_SUPER_SIGNAL:inner_state
2019-07-26 11:37:22,683 DEBUG:S: [ao1] EXIT_SIGNAL:outer_state
2019-07-26 11:37:22,683 DEBUG:S: [ao1] exiting outer_state
2019-07-26 11:37:22,683 DEBUG:S: [ao1] ENTRY_SIGNAL:outer_state
2019-07-26 11:37:22,684 DEBUG:S: [ao2] Hook:inner_state
2019-07-26 11:37:22,684 DEBUG:S: [ao1] hello from outer_state
2019-07-26 11:37:22,684 DEBUG:S: [ao2] Hook:outer_state
2019-07-26 11:37:22,684 DEBUG:S: [ao1] INIT_SIGNAL:outer_state
2019-07-26 11:37:22,684 DEBUG:S: [ao2] run some code, but don't transition
2019-07-26 11:37:22,684 DEBUG:S: [ao1] init
2019-07-26 11:37:22,685 DEBUG:S: [ao2] Hook:outer_state:HOOK
2019-07-26 11:37:22,685 DEBUG:S: [ao1] SEARCH_FOR_SUPER_SIGNAL:inner_state
2019-07-26 11:37:22,685 DEBUG:S: [ao2] <- Queued:(1) Deferred:(0)
2019-07-26 11:37:22,685 DEBUG:S: [ao1] ENTRY_SIGNAL:inner_state
2019-07-26 11:37:22,686 DEBUG:T: [ao2] e->Reset() inner_state->inner_state
2019-07-26 11:37:22,687 DEBUG:S: [ao1] hello from inner_state
2019-07-26 11:37:22,687 DEBUG:S: [ao2] Reset:inner_state
2019-07-26 11:37:22,687 DEBUG:S: [ao1] INIT_SIGNAL:inner_state
2019-07-26 11:37:22,687 DEBUG:S: [ao2] Reset:outer_state
2019-07-26 11:37:22,687 DEBUG:S: [ao1] <- Queued:(0) Deferred:(0)
2019-07-26 11:37:22,688 DEBUG:S: [ao2] resetting the chart
2019-07-26 11:37:22,688 DEBUG:S: [ao2] EXIT_SIGNAL:inner_state
2019-07-26 11:37:22,688 DEBUG:S: [ao2] exiting inner_state
2019-07-26 11:37:22,688 DEBUG:S: [ao2] SEARCH_FOR_SUPER_SIGNAL:inner_state
2019-07-26 11:37:22,688 DEBUG:S: [ao2] EXIT_SIGNAL:outer_state
2019-07-26 11:37:22,689 DEBUG:S: [ao2] exiting outer_state
2019-07-26 11:37:22,689 DEBUG:S: [ao2] ENTRY_SIGNAL:outer_state
2019-07-26 11:37:22,689 DEBUG:S: [ao2] hello from outer_state
2019-07-26 11:37:22,689 DEBUG:S: [ao2] INIT_SIGNAL:outer_state
2019-07-26 11:37:22,689 DEBUG:S: [ao2] init
2019-07-26 11:37:22,689 DEBUG:S: [ao2] SEARCH_FOR_SUPER_SIGNAL:inner_state
2019-07-26 11:37:22,689 DEBUG:S: [ao2] ENTRY_SIGNAL:inner_state
2019-07-26 11:37:22,690 DEBUG:S: [ao2] hello from inner_state
2019-07-26 11:37:22,690 DEBUG:S: [ao2] INIT_SIGNAL:inner_state
2019-07-26 11:37:22,690 DEBUG:S: [ao2] <- Queued:(0) Deferred:(0)
That’s a lot of information. Suppose we just wanted to see the ao1
trace:
cat simple_state_9.log | grep T:*.a01
This would result in the following output:
2019-07-26 11:37:22,674 DEBUG:T: [ao1] e->start_at() top->inner_state
2019-07-26 11:37:22,682 DEBUG:T: [ao1] e->Reset() inner_state->inner_state
If you wanted to see the spy of a02
, you could grep S:*.a02
… etc.
So far we have seen how to create state functions, how to link them to an
Activeobject
, which collectively makes a statechart. If you would like to
see a comprehensive example of all of the signal pathways supported by the Miro
Samek event processor, look at this example.
Making a Statechart from a class#
Is there a way to just make a statechart by instantiating a class?
Yes, this is what the Factory
class is for. It links a set of signals to
static methods; and assigns this linked collection to a state. You do this once
per state, then you nest the states within one another; and the resulting object
is a statechart. To start its thread, you call its start_at method, just as
you would with an ActiveObject
.
Let’s rebuild our previous example using the miros.Factory
class.
Note
The above image is probably breaking the UML specification, since I’m packing an HSM diagram into a class icon. But I don’t care, since I have found that this is a compact way of drawing my design intentions.
The diagram is saying that the FactoryInstrumentationToLog
class has a
logging object and is inherited from the Factory
class, and the Factory
class is inherited from the ActiveObject
class. It has two attributes,
live_spy
and live_trace
and three methods, trace_callback
,
spy_callback
and start_at
.
I don’t know how to draw UML to describe that I want this class to start its
statemachine in two different ways, so I write the start_at
code onto the
diagram to remind myself that I’m thinking this way.
Within the class, we see the same state machine we described in the
simple_state_9.py
code listing.
Here is the above design in code:
# simple_state_10.py
import re
import time
import logging
from functools import partial
from miros import Event
from miros import signals
from miros import Factory
from miros import return_status
class FactoryInstrumentationToLog(Factory):
def __init__(self, name, log_file_name=None,
live_trace=None, live_spy=None):
super().__init__(name)
self.live_trace = \
False if live_trace == None else live_trace
self.live_spy = \
False if live_spy == None else live_spy
self.log_file_name = \
'simple_state_10.log' if log_file_name == None else log_file_name
logging.basicConfig(
format='%(asctime)s %(levelname)s:%(message)s',
filename=self.log_file_name,
level=logging.DEBUG)
self.register_live_spy_callback(partial(self.spy_callback))
self.register_live_trace_callback(partial(self.trace_callback))
self.outer_state = self.create(state="outer_state"). \
catch(signal=signals.ENTRY_SIGNAL,
handler=self.outer_state_entry_signal). \
catch(signal=signals.INIT_SIGNAL,
handler=self.outer_state_init_signal). \
catch(signal=signals.Hook,
handler=self.outer_state_hook). \
catch(signal=signals.Reset,
handler=self.outer_state_reset). \
catch(signal=signals.EXIT_SIGNAL,
handler=self.outer_state_exit_signal). \
to_method()
self.inner_state = self.create(state="inner_state"). \
catch(signal=signals.ENTRY_SIGNAL,
handler=self.inner_state_entry_signal). \
catch(signal=signals.EXIT_SIGNAL,
handler=self.inner_state_exit_signal). \
to_method()
self.nest(self.outer_state, parent=None). \
nest(self.inner_state, parent=self.outer_state)
def trace_callback(self, trace):
'''trace without datetime-stamp'''
trace_without_datetime = re.search(r'(\[.+\]) (\[.+\].+)', trace).group(2)
logging.debug("T: " + trace_without_datetime)
def spy_callback(self, spy):
'''spy with machine name pre-pended'''
logging.debug("S: [{}] {}".format(self.name, spy))
@staticmethod
def outer_state_entry_signal(chart, e):
chart.scribble("hello from outer_state")
status = return_status.HANDLED
return status
@staticmethod
def outer_state_init_signal(chart, e):
chart.scribble("init")
status = chart.trans(chart.inner_state)
return status
@staticmethod
def outer_state_hook(chart, e):
status = return_status.HANDLED
chart.scribble("run some code, but don't transition")
return status
@staticmethod
def outer_state_reset(chart, e):
status = chart.trans(chart.outer_state)
return status
@staticmethod
def outer_state_exit_signal(chart, e):
status = return_status.HANDLED
chart.scribble("exiting the outer_state")
return status
@staticmethod
def inner_state_entry_signal(chart, e):
status = return_status.HANDLED
chart.scribble("hello from inner_state")
return status
@staticmethod
def inner_state_exit_signal(chart, e):
status = return_status.HANDLED
chart.scribble("exiting inner_state")
return status
if __name__ == '__main__':
f1 = FactoryInstrumentationToLog(
"f1",
live_trace=True,
live_spy=True
)
f2 = FactoryInstrumentationToLog(
"f2",
live_trace=True,
live_spy=True
)
f1.start_at(f1.outer_state)
f1.post_fifo(Event(signal=signals.Hook))
f1.post_fifo(Event(signal=signals.Reset))
f2.start_at(f2.inner_state)
f2.post_fifo(Event(signal=signals.Hook))
f2.post_fifo(Event(signal=signals.Reset))
# let the threads catch up before we exit main
time.sleep(0.01)
The benefit of programming a statechart this way is in its containment. You
don’t have functions drifting in your package’s name space, they are nicely
contained as static methods within your statechart’s class. In addition to
this, you no longer have to manipulate the temp.fun
attribute of the event
processor, this complexity is hidden within the Factory
object’s
state-function-manufacturing process. To move the state functions into a class,
you can just add a @staticmethod
decorator on top of them. Any static
method is just a function forced into a class.
If you like, you can point the handler
of the Factory’s create
method to
a method instead of a function (or staticmethod). To write the code this way
only slightly changes our diagram, we need to replace all chart
variables
with self
:
To write this design, our state functions become state methods (this feature was added in miros 4.1.2):
# simple_state_11.py
import re
import time
import logging
from functools import partial
from miros import Event
from miros import signals
from miros import Factory
from miros import return_status
class FactoryInstrumentationToLog(Factory):
def __init__(self, name, log_file_name=None,
live_trace=None, live_spy=None):
super().__init__(name)
self.live_trace = \
False if live_trace == None else live_trace
self.live_spy = \
False if live_spy == None else live_spy
self.log_file_name = \
'simple_state_11.log' if log_file_name == None else log_file_name
logging.basicConfig(
format='%(asctime)s %(levelname)s:%(message)s',
filename=self.log_file_name,
level=logging.DEBUG)
self.register_live_spy_callback(partial(self.spy_callback))
self.register_live_trace_callback(partial(self.trace_callback))
self.outer_state = self.create(state="outer_state"). \
catch(signal=signals.ENTRY_SIGNAL,
handler=self.outer_state_entry_signal). \
catch(signal=signals.INIT_SIGNAL,
handler=self.outer_state_init_signal). \
catch(signal=signals.Hook,
handler=self.outer_state_hook). \
catch(signal=signals.Reset,
handler=self.outer_state_reset). \
catch(signal=signals.EXIT_SIGNAL,
handler=self.outer_state_exit_signal). \
to_method()
self.inner_state = self.create(state="inner_state"). \
catch(signal=signals.ENTRY_SIGNAL,
handler=self.inner_state_entry_signal). \
catch(signal=signals.EXIT_SIGNAL,
handler=self.inner_state_exit_signal). \
to_method()
self.nest(self.outer_state, parent=None). \
nest(self.inner_state, parent=self.outer_state)
def trace_callback(self, trace):
'''trace without datetime-stamp'''
trace_without_datetime = re.search(r'(\[.+\]) (\[.+\].+)', trace).group(2)
logging.debug("T: " + trace_without_datetime)
def spy_callback(self, spy):
'''spy with machine name pre-pended'''
logging.debug("S: [{}] {}".format(self.name, spy))
def outer_state_entry_signal(self, e):
self.scribble("hello from outer_state")
status = return_status.HANDLED
return status
def outer_state_init_signal(self, e):
self.scribble("init")
status = self.trans(self.inner_state)
return status
def outer_state_hook(self, e):
status = return_status.HANDLED
self.scribble("run some code, but don't transition")
return status
def outer_state_reset(self, e):
status = self.trans(self.outer_state)
return status
def outer_state_exit_signal(self, e):
status = return_status.HANDLED
self.scribble("exiting the outer_state")
return status
def inner_state_entry_signal(self, e):
status = return_status.HANDLED
self.scribble("hello from inner_state")
return status
def inner_state_exit_signal(self, e):
status = return_status.HANDLED
self.scribble("exiting inner_state")
return status
if __name__ == '__main__':
f1 = FactoryInstrumentationToLog(
"f1",
live_trace=True,
live_spy=True
)
f2 = FactoryInstrumentationToLog(
"f2",
live_trace=True,
live_spy=True
)
f1.start_at(f1.outer_state)
f1.post_fifo(Event(signal=signals.Hook))
f1.post_fifo(Event(signal=signals.Reset))
f2.start_at(f2.inner_state)
f2.post_fifo(Event(signal=signals.Hook))
f2.post_fifo(Event(signal=signals.Reset))
# let the threads catch up before we exit main
time.sleep(0.01)
Note
It’s nice to look at code that looks like a typical python class. But be
warned, the methods you assigned to your create handlers will actually be run in
a separate thread from your __init__
and start_at
methods. The
trace_callback
and spy_callback
and all of your state handler methods
will run in the statechart’s thread. This mostly isn’t a problem, unless you
are trying to get data from your statechart into your main program. We will see
how to do this shortly
If we ran the code and looked at its log file we would see:
2019-07-30 06:25:43,725 DEBUG:T: [f1] e->start_at() top->inner_state
2019-07-30 06:25:43,726 DEBUG:S: [f1] START
2019-07-30 06:25:43,726 DEBUG:S: [f1] SEARCH_FOR_SUPER_SIGNAL:outer_state
2019-07-30 06:25:43,726 DEBUG:S: [f1] ENTRY_SIGNAL:outer_state
2019-07-30 06:25:43,726 DEBUG:S: [f1] hello from outer_state
2019-07-30 06:25:43,726 DEBUG:S: [f1] INIT_SIGNAL:outer_state
2019-07-30 06:25:43,726 DEBUG:S: [f1] init
2019-07-30 06:25:43,726 DEBUG:S: [f1] SEARCH_FOR_SUPER_SIGNAL:inner_state
2019-07-30 06:25:43,727 DEBUG:S: [f1] ENTRY_SIGNAL:inner_state
2019-07-30 06:25:43,727 DEBUG:S: [f1] hello from inner_state
2019-07-30 06:25:43,727 DEBUG:S: [f1] INIT_SIGNAL:inner_state
2019-07-30 06:25:43,727 DEBUG:S: [f1] <- Queued:(0) Deferred:(0)
2019-07-30 06:25:43,729 DEBUG:T: [f2] e->start_at() top->inner_state
2019-07-30 06:25:43,729 DEBUG:S: [f2] START
2019-07-30 06:25:43,729 DEBUG:S: [f2] SEARCH_FOR_SUPER_SIGNAL:inner_state
2019-07-30 06:25:43,730 DEBUG:S: [f2] SEARCH_FOR_SUPER_SIGNAL:outer_state
2019-07-30 06:25:43,730 DEBUG:S: [f2] ENTRY_SIGNAL:outer_state
2019-07-30 06:25:43,730 DEBUG:S: [f2] hello from outer_state
2019-07-30 06:25:43,730 DEBUG:S: [f2] ENTRY_SIGNAL:inner_state
2019-07-30 06:25:43,730 DEBUG:S: [f2] hello from inner_state
2019-07-30 06:25:43,730 DEBUG:S: [f2] INIT_SIGNAL:inner_state
2019-07-30 06:25:43,730 DEBUG:S: [f2] <- Queued:(0) Deferred:(0)
2019-07-30 06:25:43,731 DEBUG:S: [f2] Hook:inner_state
2019-07-30 06:25:43,731 DEBUG:S: [f2] Hook:outer_state
2019-07-30 06:25:43,731 DEBUG:S: [f2] run some code, but don't transition
2019-07-30 06:25:43,732 DEBUG:S: [f2] Hook:outer_state:HOOK
2019-07-30 06:25:43,732 DEBUG:S: [f2] <- Queued:(1) Deferred:(0)
2019-07-30 06:25:43,733 DEBUG:T: [f2] e->Reset() inner_state->inner_state
2019-07-30 06:25:43,733 DEBUG:S: [f2] Reset:inner_state
2019-07-30 06:25:43,733 DEBUG:S: [f2] Reset:outer_state
2019-07-30 06:25:43,733 DEBUG:S: [f2] EXIT_SIGNAL:inner_state
2019-07-30 06:25:43,734 DEBUG:S: [f2] exiting inner_state
2019-07-30 06:25:43,734 DEBUG:S: [f2] SEARCH_FOR_SUPER_SIGNAL:inner_state
2019-07-30 06:25:43,734 DEBUG:S: [f2] EXIT_SIGNAL:outer_state
2019-07-30 06:25:43,734 DEBUG:S: [f2] exiting the outer_state
2019-07-30 06:25:43,734 DEBUG:S: [f2] ENTRY_SIGNAL:outer_state
2019-07-30 06:25:43,734 DEBUG:S: [f2] hello from outer_state
2019-07-30 06:25:43,734 DEBUG:S: [f2] INIT_SIGNAL:outer_state
2019-07-30 06:25:43,735 DEBUG:S: [f2] init
2019-07-30 06:25:43,735 DEBUG:S: [f2] SEARCH_FOR_SUPER_SIGNAL:inner_state
2019-07-30 06:25:43,735 DEBUG:S: [f2] ENTRY_SIGNAL:inner_state
2019-07-30 06:25:43,735 DEBUG:S: [f2] hello from inner_state
2019-07-30 06:25:43,735 DEBUG:S: [f2] INIT_SIGNAL:inner_state
2019-07-30 06:25:43,736 DEBUG:S: [f1] Hook:inner_state
2019-07-30 06:25:43,736 DEBUG:S: [f2] <- Queued:(0) Deferred:(0)
2019-07-30 06:25:43,736 DEBUG:S: [f1] Hook:outer_state
2019-07-30 06:25:43,736 DEBUG:S: [f1] run some code, but don't transition
2019-07-30 06:25:43,736 DEBUG:S: [f1] Hook:outer_state:HOOK
2019-07-30 06:25:43,737 DEBUG:S: [f1] <- Queued:(1) Deferred:(0)
2019-07-30 06:25:43,738 DEBUG:T: [f1] e->Reset() inner_state->inner_state
2019-07-30 06:25:43,738 DEBUG:S: [f1] Reset:inner_state
2019-07-30 06:25:43,738 DEBUG:S: [f1] Reset:outer_state
2019-07-30 06:25:43,738 DEBUG:S: [f1] EXIT_SIGNAL:inner_state
2019-07-30 06:25:43,739 DEBUG:S: [f1] exiting inner_state
2019-07-30 06:25:43,739 DEBUG:S: [f1] SEARCH_FOR_SUPER_SIGNAL:inner_state
2019-07-30 06:25:43,739 DEBUG:S: [f1] EXIT_SIGNAL:outer_state
2019-07-30 06:25:43,739 DEBUG:S: [f1] exiting the outer_state
2019-07-30 06:25:43,739 DEBUG:S: [f1] ENTRY_SIGNAL:outer_state
2019-07-30 06:25:43,739 DEBUG:S: [f1] hello from outer_state
2019-07-30 06:25:43,739 DEBUG:S: [f1] INIT_SIGNAL:outer_state
2019-07-30 06:25:43,739 DEBUG:S: [f1] init
2019-07-30 06:25:43,740 DEBUG:S: [f1] SEARCH_FOR_SUPER_SIGNAL:inner_state
2019-07-30 06:25:43,740 DEBUG:S: [f1] ENTRY_SIGNAL:inner_state
2019-07-30 06:25:43,740 DEBUG:S: [f1] hello from inner_state
2019-07-30 06:25:43,740 DEBUG:S: [f1] INIT_SIGNAL:inner_state
2019-07-30 06:25:43,740 DEBUG:S: [f1] <- Queued:(0) Deferred:(0)
2019-08-01 06:28:10,237 DEBUG:T: [f1] e->start_at() top->inner_state
2019-08-01 06:28:10,238 DEBUG:S: [f1] START
2019-08-01 06:28:10,238 DEBUG:S: [f1] SEARCH_FOR_SUPER_SIGNAL:outer_state
2019-08-01 06:28:10,238 DEBUG:S: [f1] ENTRY_SIGNAL:outer_state
2019-08-01 06:28:10,238 DEBUG:S: [f1] hello from outer_state
2019-08-01 06:28:10,238 DEBUG:S: [f1] INIT_SIGNAL:outer_state
2019-08-01 06:28:10,238 DEBUG:S: [f1] init
2019-08-01 06:28:10,239 DEBUG:S: [f1] SEARCH_FOR_SUPER_SIGNAL:inner_state
2019-08-01 06:28:10,239 DEBUG:S: [f1] ENTRY_SIGNAL:inner_state
2019-08-01 06:28:10,239 DEBUG:S: [f1] hello from inner_state
2019-08-01 06:28:10,239 DEBUG:S: [f1] INIT_SIGNAL:inner_state
2019-08-01 06:28:10,239 DEBUG:S: [f1] <- Queued:(0) Deferred:(0)
2019-08-01 06:28:10,241 DEBUG:T: [f2] e->start_at() top->inner_state
2019-08-01 06:28:10,241 DEBUG:S: [f1] Hook:inner_state
2019-08-01 06:28:10,241 DEBUG:S: [f2] START
2019-08-01 06:28:10,242 DEBUG:S: [f1] Hook:outer_state
2019-08-01 06:28:10,242 DEBUG:S: [f2] SEARCH_FOR_SUPER_SIGNAL:inner_state
2019-08-01 06:28:10,242 DEBUG:S: [f1] run some code, but don't transition
2019-08-01 06:28:10,242 DEBUG:S: [f2] SEARCH_FOR_SUPER_SIGNAL:outer_state
2019-08-01 06:28:10,242 DEBUG:S: [f1] Hook:outer_state:HOOK
2019-08-01 06:28:10,243 DEBUG:S: [f2] ENTRY_SIGNAL:outer_state
2019-08-01 06:28:10,243 DEBUG:S: [f1] <- Queued:(1) Deferred:(0)
2019-08-01 06:28:10,243 DEBUG:S: [f2] hello from outer_state
2019-08-01 06:28:10,245 DEBUG:T: [f1] e->Reset() inner_state->inner_state
2019-08-01 06:28:10,245 DEBUG:S: [f2] ENTRY_SIGNAL:inner_state
2019-08-01 06:28:10,245 DEBUG:S: [f1] Reset:inner_state
2019-08-01 06:28:10,245 DEBUG:S: [f2] hello from inner_state
2019-08-01 06:28:10,245 DEBUG:S: [f1] Reset:outer_state
2019-08-01 06:28:10,246 DEBUG:S: [f2] INIT_SIGNAL:inner_state
2019-08-01 06:28:10,246 DEBUG:S: [f1] EXIT_SIGNAL:inner_state
2019-08-01 06:28:10,246 DEBUG:S: [f2] <- Queued:(0) Deferred:(0)
2019-08-01 06:28:10,246 DEBUG:S: [f1] exiting inner_state
2019-08-01 06:28:10,247 DEBUG:S: [f1] SEARCH_FOR_SUPER_SIGNAL:inner_state
2019-08-01 06:28:10,247 DEBUG:S: [f1] EXIT_SIGNAL:outer_state
2019-08-01 06:28:10,247 DEBUG:S: [f1] exiting the outer_state
2019-08-01 06:28:10,247 DEBUG:S: [f1] ENTRY_SIGNAL:outer_state
2019-08-01 06:28:10,247 DEBUG:S: [f1] hello from outer_state
2019-08-01 06:28:10,247 DEBUG:S: [f1] INIT_SIGNAL:outer_state
2019-08-01 06:28:10,248 DEBUG:S: [f1] init
2019-08-01 06:28:10,248 DEBUG:S: [f1] SEARCH_FOR_SUPER_SIGNAL:inner_state
2019-08-01 06:28:10,248 DEBUG:S: [f1] ENTRY_SIGNAL:inner_state
2019-08-01 06:28:10,248 DEBUG:S: [f1] hello from inner_state
2019-08-01 06:28:10,248 DEBUG:S: [f1] INIT_SIGNAL:inner_state
2019-08-01 06:28:10,248 DEBUG:S: [f1] <- Queued:(0) Deferred:(0)
2019-08-01 06:28:10,249 DEBUG:S: [f2] Hook:inner_state
2019-08-01 06:28:10,249 DEBUG:S: [f2] Hook:outer_state
2019-08-01 06:28:10,249 DEBUG:S: [f2] run some code, but don't transition
2019-08-01 06:28:10,249 DEBUG:S: [f2] Hook:outer_state:HOOK
2019-08-01 06:28:10,250 DEBUG:S: [f2] <- Queued:(1) Deferred:(0)
2019-08-01 06:28:10,251 DEBUG:T: [f2] e->Reset() inner_state->inner_state
2019-08-01 06:28:10,251 DEBUG:S: [f2] Reset:inner_state
2019-08-01 06:28:10,251 DEBUG:S: [f2] Reset:outer_state
2019-08-01 06:28:10,251 DEBUG:S: [f2] EXIT_SIGNAL:inner_state
2019-08-01 06:28:10,252 DEBUG:S: [f2] exiting inner_state
2019-08-01 06:28:10,252 DEBUG:S: [f2] SEARCH_FOR_SUPER_SIGNAL:inner_state
2019-08-01 06:28:10,252 DEBUG:S: [f2] EXIT_SIGNAL:outer_state
2019-08-01 06:28:10,252 DEBUG:S: [f2] exiting the outer_state
2019-08-01 06:28:10,252 DEBUG:S: [f2] ENTRY_SIGNAL:outer_state
2019-08-01 06:28:10,252 DEBUG:S: [f2] hello from outer_state
2019-08-01 06:28:10,252 DEBUG:S: [f2] INIT_SIGNAL:outer_state
2019-08-01 06:28:10,253 DEBUG:S: [f2] init
2019-08-01 06:28:10,253 DEBUG:S: [f2] SEARCH_FOR_SUPER_SIGNAL:inner_state
2019-08-01 06:28:10,253 DEBUG:S: [f2] ENTRY_SIGNAL:inner_state
2019-08-01 06:28:10,253 DEBUG:S: [f2] hello from inner_state
2019-08-01 06:28:10,253 DEBUG:S: [f2] INIT_SIGNAL:inner_state
2019-08-01 06:28:10,253 DEBUG:S: [f2] <- Queued:(0) Deferred:(0)
You can see as much information as you like about your statechart dynamics. Typically I only turn on the spy when I’m debugging a problem; and I’ll leave the trace on when I’m trying to see how a statechart behaves.
Communication between Statecharts#
To have two different statecharts communicate with one another we use the publish and subscribe methods (available in the ActiveObject and its subclasses). Let’s adjust our example a bit so that we can send a Broadcast event to one statechart, which will cause both charts to act.
Here is the code (highlighting pub/sub/action code):
# simple_state_12.py
import re
import time
import logging
from functools import partial
from miros import Event
from miros import signals
from miros import Factory
from miros import return_status
class FactoryInstrumentationToLog(Factory):
def __init__(self, name, log_file_name=None,
live_trace=None, live_spy=None):
super().__init__(name)
self.live_trace = \
False if live_trace == None else live_trace
self.live_spy = \
False if live_spy == None else live_spy
self.log_file_name = \
'simple_state_12.log' if log_file_name == None else log_file_name
# clear our log every time we run this program
with open(self.log_file_name, "w") as fp:
fp.write("")
logging.basicConfig(
format='%(asctime)s %(levelname)s:%(message)s',
filename=self.log_file_name,
level=logging.DEBUG)
self.register_live_spy_callback(partial(self.spy_callback))
self.register_live_trace_callback(partial(self.trace_callback))
self.outer_state = self.create(state="outer_state"). \
catch(signal=signals.ENTRY_SIGNAL,
handler=self.outer_state_entry_signal). \
catch(signal=signals.INIT_SIGNAL,
handler=self.outer_state_init_signal). \
catch(signal=signals.Hook,
handler=self.outer_state_hook). \
catch(signal=signals.Send_Broadcast, \
handler=self.outer_state_send_broadcast). \
catch(signal=signals.BROADCAST, \
handler=self.outer_state_broadcast). \
catch(signal=signals.Reset,
handler=self.outer_state_reset). \
catch(signal=signals.EXIT_SIGNAL,
handler=self.outer_state_exit_signal). \
to_method()
self.inner_state = self.create(state="inner_state"). \
catch(signal=signals.ENTRY_SIGNAL,
handler=self.inner_state_entry_signal). \
catch(signal=signals.EXIT_SIGNAL,
handler=self.inner_state_exit_signal). \
to_method()
self.nest(self.outer_state, parent=None). \
nest(self.inner_state, parent=self.outer_state)
def trace_callback(self, trace):
'''trace without datetime-stamp'''
trace_without_datetime = re.search(r'(\[.+\]) (\[.+\].+)', trace).group(2)
logging.debug("T: " + trace_without_datetime)
def spy_callback(self, spy):
'''spy with machine name pre-pended'''
logging.debug("S: [{}] {}".format(self.name, spy))
def outer_state_entry_signal(self, e):
self.subscribe(Event(signal=signals.BROADCAST))
self.scribble("hello from outer_state")
status = return_status.HANDLED
return status
def outer_state_init_signal(self, e):
self.scribble("init")
status = self.trans(self.inner_state)
return status
def outer_state_hook(self, e):
status = return_status.HANDLED
self.scribble("run some code, but don't transition")
return status
def outer_state_send_broadcast(self, e):
status = return_status.HANDLED
self.publish(Event(signal=signals.BROADCAST))
return status
def outer_state_broadcast(self, e):
status = return_status.HANDLED
self.scribble("received broadcast")
return status
def outer_state_reset(self, e):
status = self.trans(self.outer_state)
return status
def outer_state_exit_signal(self, e):
status = return_status.HANDLED
self.scribble("exiting the outer_state")
return status
def inner_state_entry_signal(self, e):
status = return_status.HANDLED
self.scribble("hello from inner_state")
return status
def inner_state_exit_signal(self, e):
status = return_status.HANDLED
self.scribble("exiting inner_state")
return status
if __name__ == '__main__':
f1 = FactoryInstrumentationToLog(
"f1",
live_trace=True,
live_spy=True
)
f2 = FactoryInstrumentationToLog(
"f2",
live_trace=True,
live_spy=True
)
f1.start_at(f1.outer_state)
f1.post_fifo(Event(signal=signals.Hook))
f1.post_fifo(Event(signal=signals.Reset))
f2.start_at(f2.inner_state)
f2.post_fifo(Event(signal=signals.Hook))
f2.post_fifo(Event(signal=signals.Reset))
f1.post_fifo(Event(signal=signals.Send_Broadcast))
# let the threads catch up before we exit main
time.sleep(0.02)
The above code posts a Send_Broadcast
event to f1, which in turn, publishes
the Broadcast
event. Since both charts subscribed to this event, they will
react to the Broadcast
event. The Broadcast
signal is attached to both
charts as a hook in their outer_state
, which means this hook’s reactive
behavior will be common for the outer_state and the inner_state. The “received
broadcast” message will be written into the spy log via the scribble
method,
if a Broadcast
event is received in either state. No state transition will
occur as a result of the reaction to a Broadcast
event; Broadcast
is a
hook and hooks don’t cause state transitions.
If we run our code, then filter its log file through a grep search pattern, we
will see that both charts received the BROADCAST
event:
1python simple_state_12.py ; cat simple_state_12.log | grep broadcast
22019-08-02 07:03:17,352 DEBUG:S: [f1] received broadcast
32019-08-02 07:03:17,355 DEBUG:S: [f2] received broadcast
So now we know how:
to build a statechart within a class
to tie the statechart instrumentation features to the logging system (or whatever you want)
to have two or more statecharts communicate with one another
to map the statechart features into functions of static methods or methods.
Can we program our states by difference?
Overload a State in a Subclass#
Suppose our specification poses a problem that can be broken into two or more subdesigns, and these designs are very similar. Since we know how to tie our event handlers to methods in a class, we can just subclass one statechart and overload the event handlers that we need to change.
Here is how to draw this as a UML diagram. The F1 class is our first completed subdesign and F2 is just like F1, except we change the behavior of the inner_state’s entry and exit conditions:
The diagram mostly tells us what is going on, and I have added a few notes to belabor the point. Here is the code, I have highlighted the statechart-by-difference part of the design:
# simple_state_13.py
import re
import time
import logging
from functools import partial
from miros import Event
from miros import signals
from miros import Factory
from miros import return_status
class F1(Factory):
def __init__(self, name, log_file_name=None,
live_trace=None, live_spy=None):
super().__init__(name)
self.live_trace = \
False if live_trace == None else live_trace
self.live_spy = \
False if live_spy == None else live_spy
self.log_file_name = \
'simple_state_13.log' if log_file_name == None else log_file_name
# clear our log every time we run this program
with open(self.log_file_name, "w") as fp:
fp.write("")
logging.basicConfig(
format='%(asctime)s %(levelname)s:%(message)s',
filename=self.log_file_name,
level=logging.DEBUG)
self.register_live_spy_callback(partial(self.spy_callback))
self.register_live_trace_callback(partial(self.trace_callback))
self.outer_state = self.create(state="outer_state"). \
catch(signal=signals.ENTRY_SIGNAL,
handler=self.outer_state_entry_signal). \
catch(signal=signals.INIT_SIGNAL,
handler=self.outer_state_init_signal). \
catch(signal=signals.Hook,
handler=self.outer_state_hook). \
catch(signal=signals.Send_Broadcast, \
handler=self.outer_state_send_broadcast). \
catch(signal=signals.BROADCAST, \
handler=self.outer_state_broadcast). \
catch(signal=signals.Reset,
handler=self.outer_state_reset). \
catch(signal=signals.EXIT_SIGNAL,
handler=self.outer_state_exit_signal). \
to_method()
self.inner_state = self.create(state="inner_state"). \
catch(signal=signals.ENTRY_SIGNAL,
handler=self.inner_state_entry_signal). \
catch(signal=signals.EXIT_SIGNAL,
handler=self.inner_state_exit_signal). \
to_method()
self.nest(self.outer_state, parent=None). \
nest(self.inner_state, parent=self.outer_state)
def trace_callback(self, trace):
'''trace without datetime-stamp'''
trace_without_datetime = re.search(r'(\[.+\]) (\[.+\].+)', trace).group(2)
logging.debug("T: " + trace_without_datetime)
def spy_callback(self, spy):
'''spy with machine name pre-pended'''
logging.debug("S: [{}] {}".format(self.name, spy))
def outer_state_entry_signal(self, e):
self.subscribe(Event(signal=signals.BROADCAST))
self.scribble("hello from outer_state")
status = return_status.HANDLED
return status
def outer_state_init_signal(self, e):
self.scribble("init")
status = self.trans(self.inner_state)
return status
def outer_state_hook(self, e):
status = return_status.HANDLED
self.scribble("run some code, but don't transition")
return status
def outer_state_send_broadcast(self, e):
status = return_status.HANDLED
self.publish(Event(signal=signals.BROADCAST))
return status
def outer_state_broadcast(self, e):
status = return_status.HANDLED
self.scribble("received broadcast")
return status
def outer_state_reset(self, e):
status = self.trans(self.outer_state)
return status
def outer_state_exit_signal(self, e):
status = return_status.HANDLED
self.scribble("exiting the outer_state")
return status
def inner_state_entry_signal(self, e):
status = return_status.HANDLED
self.scribble("hello from inner_state")
return status
def inner_state_exit_signal(self, e):
status = return_status.HANDLED
self.scribble("exiting inner_state")
return status
class F2(F1):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def inner_state_entry_signal(self, e):
status = return_status.HANDLED
self.scribble("hello from new inner_state")
return status
def inner_state_exit_signal(self, e):
status = return_status.HANDLED
self.scribble("exiting new inner_state")
return status
if __name__ == '__main__':
f1 = F1(
"f1",
live_trace=True,
live_spy=True
)
f2 = F2(
"f2",
live_trace=True,
live_spy=True
)
f1.start_at(f1.outer_state)
f1.post_fifo(Event(signal=signals.Hook))
f1.post_fifo(Event(signal=signals.Reset))
f2.start_at(f2.inner_state)
f2.post_fifo(Event(signal=signals.Hook))
f2.post_fifo(Event(signal=signals.Reset))
f1.post_fifo(Event(signal=signals.Send_Broadcast))
# let the threads catch up before we exit main
time.sleep(0.02)
This kind of abstraction requires a lot of understanding on the part of the maintenance developer. Statechart code is already hard enough to understand without a diagram, but now parts of our diagram are missing too: to understand one diagram we need to reference another.
If you find that your subdesign requirements change a lot you might want to just copy its superclass’s code into a flat, easy-to-read/easy-to-change form, at the cost of repeating yourself a bit. Then copy your diagram as a second stand-alone design artifact. Subclassing is software coupling, so you will have to weigh the engineering-trade-offs as you build up your system.
One-Shots, Multi-Shots and Heartbeats#
We have seen that we can post events using the post_fifo
(first in first
out) method call. If your event needs to push itself to the front of the event
queue (force itself to have the highest priority), you would use the
post_lifo
(last in first out) method.
The miros library can be used to send events at regular time intervals. The
number of times these events are sent and the regular time duration between
these events are configurable. To keep the library’s api simple, the
post_fifo
and post_lifo
methods are used for this feature. If you just
give a posting method an event it will put it into the event queue. But if you
give the posting method additional timing information, it will create a
background thread and program that thread to post your event to your statechart
using your timing specification.
To demonstrate this feature, I’ll adjust the design so that between the outer_state and inner_state there will be a middle_state. The purpose of the middle_state will be to add a one second delay between the transition into the inner_state from the outer_state. The code to make a delayed event work this way is called a one-shot (if it fired more than once it would be called a multishot, if it fired at a regular interval forever, it would be called a heartbeat).
Note
Our F2
statechart is blissfully unaware the we have made this change to it,
as it only has visibility to the inner_state event handlers.
In our design we use the post_fifo
method to make the one-shot.
The post_fifo
/post_lifo
time features can be used within, or outside of,
a statechart. In the following code example I show a one shot within (Ready)
and outside of (Reset for f1) of a statechart. The code that has been changed
from the previous example has been highlighted.
# simple_state_14.py
import re
import time
import logging
from functools import partial
from miros import Event
from miros import signals
from miros import Factory
from miros import return_status
class F1(Factory):
def __init__(self, name, log_file_name=None,
live_trace=None, live_spy=None):
super().__init__(name)
self.live_trace = \
False if live_trace == None else live_trace
self.live_spy = \
False if live_spy == None else live_spy
self.times_in_inner = 0
self.log_file_name = \
'simple_state_14.log' if log_file_name == None else log_file_name
# clear our log every time we run this program
with open(self.log_file_name, "w") as fp:
fp.write("")
logging.basicConfig(
format='%(asctime)s %(levelname)s:%(message)s',
filename=self.log_file_name,
level=logging.DEBUG)
self.register_live_spy_callback(partial(self.spy_callback))
self.register_live_trace_callback(partial(self.trace_callback))
self.outer_state = self.create(state="outer_state"). \
catch(signal=signals.ENTRY_SIGNAL,
handler=self.outer_state_entry_signal). \
catch(signal=signals.INIT_SIGNAL,
handler=self.outer_state_init_signal). \
catch(signal=signals.Hook,
handler=self.outer_state_hook). \
catch(signal=signals.Send_Broadcast, \
handler=self.outer_state_send_broadcast). \
catch(signal=signals.BROADCAST, \
handler=self.outer_state_broadcast). \
catch(signal=signals.Reset,
handler=self.outer_state_reset). \
catch(signal=signals.EXIT_SIGNAL,
handler=self.outer_state_exit_signal). \
to_method()
self.middle_state = self.create(state="middle_state"). \
catch(signal=signals.ENTRY_SIGNAL,
handler=self.middle_state_entry_signal). \
catch(signal=signals.Ready,
handler=self.middle_state_ready). \
catch(signal=signals.EXIT_SIGNAL,
handler=self.middle_state_exit_signal). \
to_method()
self.inner_state = self.create(state="inner_state"). \
catch(signal=signals.ENTRY_SIGNAL,
handler=self.inner_state_entry_signal). \
catch(signal=signals.EXIT_SIGNAL,
handler=self.inner_state_exit_signal). \
to_method()
self.nest(self.outer_state, parent=None). \
nest(self.middle_state, parent=self.outer_state). \
nest(self.inner_state, parent=self.middle_state)
def trace_callback(self, trace):
'''trace without datetime-stamp'''
trace_without_datetime = re.search(r'(\[.+\]) (\[.+\].+)', trace).group(2)
logging.debug("T: " + trace_without_datetime)
def spy_callback(self, spy):
'''spy with machine name pre-pended'''
logging.debug("S: [{}] {}".format(self.name, spy))
def outer_state_entry_signal(self, e):
self.subscribe(Event(signal=signals.BROADCAST))
self.scribble("hello from outer_state")
status = return_status.HANDLED
return status
def outer_state_init_signal(self, e):
self.scribble("init")
status = self.trans(self.middle_state)
return status
def outer_state_hook(self, e):
status = return_status.HANDLED
self.scribble("run some code, but don't transition")
return status
def outer_state_send_broadcast(self, e):
status = return_status.HANDLED
self.publish(Event(signal=signals.BROADCAST))
return status
def outer_state_broadcast(self, e):
status = return_status.HANDLED
self.scribble("received broadcast")
return status
def outer_state_reset(self, e):
status = self.trans(self.outer_state)
return status
def outer_state_exit_signal(self, e):
status = return_status.HANDLED
self.scribble("exiting the outer_state")
return status
def middle_state_entry_signal(self, e):
status = return_status.HANDLED
self.scribble("arming one-shot")
self.post_fifo(Event(signal=signals.Ready),
times=1,
period=1.0,
deferred=True)
return status
def middle_state_ready(self, e):
status = self.trans(self.inner_state)
return status
def middle_state_exit_signal(self, e):
status = return_status.HANDLED
self.cancel_events(Event(signal=signals.Ready))
return status
def inner_state_entry_signal(self, e):
status = return_status.HANDLED
self.times_in_inner += 1
self.scribble(
"hello from inner_state {}".format(self.times_in_inner))
return status
def inner_state_exit_signal(self, e):
status = return_status.HANDLED
self.scribble("exiting inner_state")
return status
class F2(F1):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def inner_state_entry_signal(self, e):
status = return_status.HANDLED
self.scribble("hello from new inner_state")
return status
def inner_state_exit_signal(self, e):
status = return_status.HANDLED
self.scribble("exiting new inner_state")
return status
if __name__ == '__main__':
f1 = F1(
"f1",
live_trace=True,
live_spy=True,
)
f2 = F2(
"f2",
live_trace=True,
live_spy=True,
)
f1.start_at(f1.outer_state)
f1.post_fifo(Event(signal=signals.Hook))
f1.post_fifo(
Event(signal=signals.Reset),
times=1,
period=2.0,
deferred=True
)
f2.start_at(f2.inner_state)
f2.post_fifo(Event(signal=signals.Hook))
f2.post_fifo(Event(signal=signals.Reset))
f1.post_fifo(Event(signal=signals.Send_Broadcast))
# delay long enough so we can see how the program behaves in time
time.sleep(4.00)
Let’s look at the design again prior to running our program:
The diagram describes how the different charts are started, but there is no
mention of the 2 second delay before we post the Reset
event to our f1 chart
from our main thread. We can see that in the exit condition of the f1
middle_state the Reset
one-shot is canceled. There is a good reason for
this: imagine that we didn’t cancel the one-shot and that we were 0.5 seconds
into our 1 second wait when a Reset
event was fired at our chart from the
main thread. This would mean that the task managing the Ready
event would
continue to run for another 0.5 seconds, then fire the Ready
event. The
Ready
event would appear to fire 0.5 seconds too early; then it would fire
again 0.5 seconds after that (from the arming of the next one-shot event). This
is weird behavior that defies the spirit of our diagram. As a rule, cancel your
one-shots in the exit conditions of the state that created them.
We have configured our live instrumentation in an NSA style: log everything so you can query it later if you feel like it. After running our program we can take a look at the different aspects, like, what the f1 trace looks like:
python simple_state_14.py ; cat simple_state_14.log | grep T:.*f1
2019-08-05 12:18:37,055 DEBUG:T: [f1] e->start_at() top->middle_state
2019-08-05 12:18:38,056 DEBUG:T: [f1] e->Ready() middle_state->inner_state
2019-08-05 12:18:39,060 DEBUG:T: [f1] e->Reset() inner_state->middle_state
2019-08-05 12:18:40,061 DEBUG:T: [f1] e->Ready() middle_state->inner_state
Here we see that 1 second after starting, the Ready
one-shot is fired and
the statechart transitions from the middle_state into the inner_state. Then 1
second later, it receives the Reset
event putting it back into the
middle_state. Then 1 second after that, the Ready
one-shot is fired again
causing a transition into the inner_state. The Ready
one shot appears to be
working as designed.
What about our new times_in_inner
code in F1? We would expect to see that
it has written to the spy scribble twice:
python simple_state_14.py ; \
cat simple_state_14.log | grep S:.*f1 | grep "hello from inner"
2019-08-05 12:23:23,054 DEBUG:S: [f1] hello from inner_state 1
2019-08-05 12:23:25,059 DEBUG:S: [f1] hello from inner_state 2
There we go, the code works. How about F2? What is its inner state saying?
python simple_state_14.py ; \
cat simple_state_14.log | grep S:.*f2 | grep "hello from new inner"
2019-08-05 12:29:55,678 DEBUG:S: [f2] hello from new inner_state
2019-08-05 12:29:56,686 DEBUG:S: [f2] hello from new inner_state
So our update to F1’s inner_state was not seen by the F2 statechart. This indicates that the inheritance structure is working. How does F2 run differently from F1?
python simple_state_14.py ; cat simple_state_14.log | grep T:.*f2
2019-08-05 12:32:08,195 DEBUG:T: [f2] e->start_at() top->inner_state
2019-08-05 12:32:08,200 DEBUG:T: [f2] e->Reset() inner_state->middle_state
2019-08-05 12:32:09,201 DEBUG:T: [f2] e->Ready() middle_state->inner_state
We see that the F2 statechart immediately transitions into the inner_state.
This is because we asked it to do so, the 1 second time delay offered by the
Ready
one-shot is by-passed, though it is still armed. But this first
Ready
one-shot is never given a chance to fire, since the Reset
is sent
to f2 immediately after it is in the inner_state. This cancels the one-shot
event. The Reset
eventually causes a transition into the middle_state,
which re-arms the Ready
one-shot, and we see a transition into the
inner_state about 1 second after the middle_state was entered.
Creating thread-safe class Attributes#
If you build a statechart using miros your program is multithreaded. This means that if you would like to access the same variable across two threads, you need to lock it so that one thread doesn’t write to it while another thread is using it.
Here is an example design of where we turn the times_in_inner
attribute into a
thread-safe property using the ThreadSafeAttributes
class (available in
miros >= 4.1.3).
Note
There is no UML drawing syntax for describing an attribute wrapped by a
property. There is no UML diagram to show a thread lock, or how multiple
inheritance works through linearization of parent classes using Python super()
.
Our UML is just a sketch, so we make a note that the times_in_inner
is a thread safe attribute and move on.
The ThreadSafeAttributes
class tries to protect you from race conditions by
inspecting the line of code where the times_in_inner
variable is used and
wrap it within a thread lock. The ThreadSafeAttributes
is limited in
its capabilities, but it will
lock the non-atomic +=
operation seen below:
# simple_state_15.py
import re
import time
import logging
from functools import partial
from miros import Event
from miros import signals
from miros import Factory
from miros import return_status
from miros import ThreadSafeAttributes
class F1(Factory, ThreadSafeAttributes):
_attributes = ['times_in_inner']
def __init__(self, name, log_file_name=None,
live_trace=None, live_spy=None):
super().__init__(name)
self.live_trace = \
False if live_trace == None else live_trace
self.live_spy = \
False if live_spy == None else live_spy
self.log_file_name = \
'simple_state_15.log' if log_file_name == None else log_file_name
# clear our log every time we run this program
with open(self.log_file_name, "w") as fp:
fp.write("")
logging.basicConfig(
format='%(asctime)s %(levelname)s:%(message)s',
filename=self.log_file_name,
level=logging.DEBUG)
self.register_live_spy_callback(partial(self.spy_callback))
self.register_live_trace_callback(partial(self.trace_callback))
self.outer_state = self.create(state="outer_state"). \
catch(signal=signals.ENTRY_SIGNAL,
handler=self.outer_state_entry_signal). \
catch(signal=signals.INIT_SIGNAL,
handler=self.outer_state_init_signal). \
catch(signal=signals.Hook,
handler=self.outer_state_hook). \
catch(signal=signals.Send_Broadcast, \
handler=self.outer_state_send_broadcast). \
catch(signal=signals.BROADCAST, \
handler=self.outer_state_broadcast). \
catch(signal=signals.Reset,
handler=self.outer_state_reset). \
catch(signal=signals.EXIT_SIGNAL,
handler=self.outer_state_exit_signal). \
to_method()
self.middle_state = self.create(state="middle_state"). \
catch(signal=signals.ENTRY_SIGNAL,
handler=self.middle_state_entry_signal). \
catch(signal=signals.Ready,
handler=self.middle_state_ready). \
catch(signal=signals.EXIT_SIGNAL,
handler=self.middle_state_exit_signal). \
to_method()
self.inner_state = self.create(state="inner_state"). \
catch(signal=signals.ENTRY_SIGNAL,
handler=self.inner_state_entry_signal). \
catch(signal=signals.EXIT_SIGNAL,
handler=self.inner_state_exit_signal). \
to_method()
self.nest(self.outer_state, parent=None). \
nest(self.middle_state, parent=self.outer_state). \
nest(self.inner_state, parent=self.middle_state)
def trace_callback(self, trace):
'''trace without datetime-stamp'''
trace_without_datetime = re.search(r'(\[.+\]) (\[.+\].+)', trace).group(2)
logging.debug("T: " + trace_without_datetime)
def spy_callback(self, spy):
'''spy with machine name pre-pended'''
logging.debug("S: [{}] {}".format(self.name, spy))
def outer_state_entry_signal(self, e):
self.subscribe(Event(signal=signals.BROADCAST))
self.scribble("hello from outer_state")
status = return_status.HANDLED
return status
def outer_state_init_signal(self, e):
self.scribble("init")
status = self.trans(self.middle_state)
return status
def outer_state_hook(self, e):
status = return_status.HANDLED
self.scribble("run some code, but don't transition")
return status
def outer_state_send_broadcast(self, e):
status = return_status.HANDLED
self.publish(Event(signal=signals.BROADCAST))
return status
def outer_state_broadcast(self, e):
status = return_status.HANDLED
self.scribble("received broadcast")
return status
def outer_state_reset(self, e):
status = self.trans(self.outer_state)
return status
def outer_state_exit_signal(self, e):
status = return_status.HANDLED
self.scribble("exiting the outer_state")
return status
def middle_state_entry_signal(self, e):
status = return_status.HANDLED
self.scribble("arming one-shot")
self.post_fifo(Event(signal=signals.Ready),
times=1,
period=1.0,
deferred=True)
return status
def middle_state_ready(self, e):
status = self.trans(self.inner_state)
return status
def middle_state_exit_signal(self, e):
status = return_status.HANDLED
self.cancel_events(Event(signal=signals.Ready))
return status
def inner_state_entry_signal(self, e):
status = return_status.HANDLED
self.times_in_inner += 1
self.scribble(
"hello from inner_state {}".format(self.times_in_inner))
return status
def inner_state_exit_signal(self, e):
status = return_status.HANDLED
self.scribble("exiting inner_state")
return status
class F2(F1):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
def inner_state_entry_signal(self, e):
status = return_status.HANDLED
self.scribble("hello from new inner_state")
return status
def inner_state_exit_signal(self, e):
status = return_status.HANDLED
self.scribble("exiting new inner_state")
return status
if __name__ == '__main__':
f1 = F1(
"f1",
live_trace=True,
live_spy=True,
)
f2 = F2(
"f2",
live_trace=True,
live_spy=True,
)
f1.start_at(f1.outer_state)
f1.post_fifo(Event(signal=signals.Hook))
f1.post_fifo(
Event(signal=signals.Reset),
times=1,
period=2.0,
deferred=True
)
f2.start_at(f2.inner_state)
f2.post_fifo(Event(signal=signals.Hook))
f2.post_fifo(Event(signal=signals.Reset))
f1.post_fifo(Event(signal=signals.Send_Broadcast))
# delay long enough so we can see how the program behaves in time
time.sleep(4.00)
# read information from other threads
print("f1 was in its inner state {} times".format(f1.times_in_inner))
print("f2 was in its inner state {} times".format(f2.times_in_inner))
So where is the lock?
It’s hidden from view. The times_in_inner
is a kind of @property
and
the variable that holds its information is protected by another hidden variable
that is the lock.
Running this program will provide the following output:
f1 was in its inner state 2 times
f2 was in its inner state 0 times
The f1 statechart properly reports how many times it was in its inner_state.
The f2 inner_state doesn’t write to the times_in_inner
property, so its
output only shows how that property was initialized.
Let’s look at the thread safe code in isolation:
from miros import Factory
# ...
from miros import ThreadSafeAttributes
class F1(Factory, ThreadSafeAttributes):
_attribute = ['times_in_inner'] # Uses a metaclass to make the
# times_in_inner property, with its
# protecting lock
def __init__(self, name, log_file_name=None,
live_trace=None, live_spy=None):
# ...
self.times_in_inner = 0
def some_state(self, e):
# ..
# Inside of the state thread small operations on the times_in_inner
# attribute can use used in a thread safe way
print(self.times_in_inner) # safe
a = self.times_in_inner # safe
self.times_in_inner = 1 # safe
self.times_in_inner += 1 # safe
self.times_in_inner += 2 * self.times_in_inner # NOT safe
self.times_in_inner = self.times_in_inner + 1 # NOT safe
self.times_in_inner = 2 * self.times_in_inner # NOT safe
The ThreadSafeAttributes
class behaves like a macro, wrapping the getting
and setting of the thread safe attribute within a thread lock. It also wraps
simple, +=
, -=
, …, <<=
statements within a lock. But that’s it.
It can’t protect other types of statements from race conditions. If you need to
use your thread safe attribute to perform more complex operations, use a
temporary variable and copy the results into the thread safe attribute when you
are done.
Better yet, share information using published events. But, the thread safe attributes feature is very useful when you are sharing information between a statechart and a thread which is not a statechart, like main.
Note
Accessing a thread safe attribute will be very slow. In the background
the ThreadSafeAttributes
uses a metaclass, and the descriptor protocol.
Within the descriptor protocol it uses the inspect
library to read the
previous line of code and compares it to a regular expression. Keep this in
mind when you use this feature.
If you have gotten this far, you have a good handle on how to use this library and all of its features. But you can take it to another level though. Your statecharts can send encrypted messages to one another and act in concert while running on different machines. To see how to do this, look at the miros-rabbitmq library.
States#
A lot of Python developers are moving away from using its object oriented
features and writing most of their code within functions which call each other.
This kind of programming is supported by miros. You can build up your
statechart by constructing a set of state functions, attaching one of them to a
miros.ActiveObject
, then using that active object as the thing to post
events to. The functions and the active object work together to form a
statechart. If you intend on porting your designs to the qp
framework, I
recommend that you write your code this way.
You can also build a statechart directly within one class, by inheriting from
the miros.Factory
. Within the __init__
method of the derived class, you
describe the states, link signals to state methods then add hierarchy using
the nest
method.
The state recipes will be broken down into two groups, state function recipes and state method recipes.
State Function Recipes#
Boiler-plate State Function Code#
from miros import Event
from miros import spy_on
from miros import signals
from miros import return_status
@spy_on
def <your_state_method_name>(chart, e):
# if your state method doesn't know what to do, it should return this
status = return_status.UNHANDLED
if e.signal == signals.ENTRY_SIGNAL:
# call your entry application code
# make sure you tell the event processor you handled this event
status = return_status.HANDLED
elif e.signal == signals.INIT_SIGNAL:
# call your initialization (big black dot) application code
# make sure you tell the event processor you handled this event
status = return_status.HANDLED
#
# Write your custom
# event handlers in here as their own elif clauses
#
elif e.signal == signals.EXIT_SIGNAL:
# call your exit application code
# make sure you tell the event processor you handled this event
status = return_status.HANDLED
else:
# this logic will run when your event processor sends an event with the
# SEARCH_FOR_SUPER_SIGNAL name
# 1) place your parent state method into the self.temp.fun
# 1.1) if this is the top-most state, use ``chart.top`` as your
# <your_parent_state_method>
chart.temp.fun = <your_parent_state_method>
# 2) make sure you return this value
status = return_status.SUPER
# return the status value
return status
If your state method didn’t include handling for the ENTRY_SIGNAL
,
INIT_SIGNAL
or EXIT_SIGNAL
, the event processor will just assume it did
and returned return_state.HANDLED.
Describing your Parent State Function#
To describe your parent state:
setting the
temp.fun
attribute of the first argument to point at their parent state.return the value of
return_state.SUPER
Generally speaking this is how it is done:
def <state_method_name>(chart, e):
# .
# .
else:
status = return_status.SUPER
chart.temp.fun = <parent_state_of_this_state_method>
return status.
If you need to define your parent state as the outermost state of your diagram, you would
set the <parent_state_of_this_state_method>
to the top
attribute of the
first argument provided to your state method:
def <state_method_name>(chart, e):
# .
# .
else:
status = return_status.SUPER
chart.temp.fun = chart.top
return status.
Passing events to the Parent State Function#
The easiest way to pass an event outward in your statechart is not to handle it
in your if-elif
clauses and let your else
clause handle it.
# Sending Event(signal=signals.B) to c1 would cause
# the parent state c to be called with this event,
# since it is not handled in the ``if-elif``
# logic structure or c1.
def c1(self, e):
status = return_status.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
status = return_status.HANDLED
elif(e.signal == signals.INIT_SIGNAL):
status = return_status.HANDLED
elif(e.signal == signals.A):
status = trans(c2)
elif(e.signal == signals.EXIT_SIGNAL):
status = return_status.HANDLED
else:
status = return_status.SUPER
self.temp.fun = self.c
return status
Another way to pass an event out to your parent state is to handle the event in
the if-elif
clause, then return return_status.UNHANDLED
to the event
processor. When it sees that your state method couldn’t handle the event it
will call it again to find its parent state and then call that parent state
method with the event that you want to trickle outward in your diagram.
# Sending Event(signal=signals.B) to c1 would cause
# the parent state c to be called with this event,
# since c1 returns a `UNHANDLED` value to the event
# processor
def c1(self, e):
status = return_status.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
status = return_status.HANDLED
elif(e.signal == signals.INIT_SIGNAL):
status = return_status.HANDLED
elif(e.signal == signals.B):
print("saw signal B, but letting it trickle through to my parent")
elif(e.signal == signals.A):
status = trans(c2)
elif(e.signal == signals.EXIT_SIGNAL):
status = return_status.HANDLED
else:
status = return_status.SUPER
self.temp.fun = self.c
return status
Transition to another State Function#
To transition to another state, use the trans
method:
# Sending Event(signal=signals.A) will cause a transition to c2
def c1(self, e):
status = return_status.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
status = return_status.HANDLED
elif(e.signal == signals.INIT_SIGNAL):
status = return_status.HANDLED
elif(e.signal == signals.A):
status = trans(c2)
elif(e.signal == signals.EXIT_SIGNAL):
status = return_status.HANDLED
else:
status = return_status.SUPER
self.temp.fun = self.c
return status
Make sure that you return the result of the call to your trans
method, or
the event processor will break.
State Function Entry Code#
To have your application code run when a state is entered place it in the
ENTRY_SIGNAL
clause of your state’s if-elif structure. An entry event will
occur anytime the event processor detects a transition from the outside to the
inside of your state method’s boundary.
# Running application code when the state is entered
def c1(self, e):
status = return_status.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
print("Running my entry application code here")
status = return_status.HANDLED
elif(e.signal == signals.INIT_SIGNAL):
status = return_status.HANDLED
elif(e.signal == signals.EXIT_SIGNAL):
status = return_status.HANDLED
else:
status = return_status.SUPER
self.temp.fun = self.c
return status
State Function Initialization Code#
To have your application code run when a state is initialized place it in the
INIT_SIGNAL
clause of your state’s if-elif structure. An init event will
occur after the entry event, if a transition is moving from the outside to the
inside of your state method’s boundary. It will also occur if there is a
transition into this state from one of its child states.
# Running application code when the state is initialized
def c1(self, e):
status = return_status.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
status = return_status.HANDLED
# BIG BLACK DOT ON DIAGRAM
elif(e.signal == signals.INIT_SIGNAL):
print("Running my init application code here")
status = return_status.HANDLED
elif(e.signal == signals.EXIT_SIGNAL):
status = return_status.HANDLED
else:
status = return_status.SUPER
self.temp.fun = self.c
return status
Note
If you only want to run initialization code and do not want your state
to immediately transition into another state, make sure you return
HANDLED
after running your application code, otherwise your
statechart will not behave properly.
The INIT_SIGNAL
handler is often used as the place where your state can
immediately transition into another state. To do this, just use the trans
method and return its result from your state method call:
# Running application code when the state is initialized
def c1(self, e):
status = return_status.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
status = return_status.HANDLED
# BIG BLACK DOT ON DIAGRAM
elif(e.signal == signals.INIT_SIGNAL):
print("Running my init application code here")
# now transition into the c2 state
status = self.trans(c2)
elif(e.signal == signals.EXIT_SIGNAL):
status = return_status.HANDLED
else:
status = return_status.SUPER
self.temp.fun = self.c
return status
Warning
If you use an init signal to transition out of your parent state an
HsmTopologyException
will be issued. Init signals should only be used to
run code with no transitions or to transition deeper into your statechart.
If you absolutely need to leave a state after entering you can post an
artificial event into the fifo/lifo. This will
cause this signal to be caught on the next rtc
process and your statechart will behave as you want it to (though, you should
probably re-visit your design).
State Function Exit Code#
To have your application code run when a state is exited place it in the
EXIT_SIGNAL
clause of your state’s if-elif structure. An exit event will
occur anytime the event processor detects a transition from the inside to the
outside of of your state method’s boundary.
# Running application code when the state is entered
def c1(self, e):
status = return_status.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
status = return_status.HANDLED
elif(e.signal == signals.INIT_SIGNAL):
status = return_status.HANDLED
elif(e.signal == signals.EXIT_SIGNAL):
print("Running my exit application code here")
status = return_status.HANDLED
else:
status = return_status.SUPER
self.temp.fun = self.c
return status
Creating a Hook in a State function#
A hook is some application code that is shared between your state method and all of its child state method’s.
Here we will create a hook in the c1 state, linking some application code to an
event with the signal name MY_HOOK
.
# Sending Event(signal=signals.A) will cause a transition to c2
def c1(self, e):
status = return_status.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
status = return_status.HANDLED
elif(e.signal == signals.INIT_SIGNAL):
status = return_status.HANDLED
elif(e.signal == signals.MY_HOOK):
print("running the code defined in c1")
status = return_status.HANDLED
elif(e.signal == signals.EXIT_SIGNAL):
status = return_status.HANDLED
else:
status = return_status.SUPER
self.temp.fun = self.top
return status
Now we will make a child state.
# Create a child state of c1
def c11(self, e):
status = return_status.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
status = return_status.HANDLED
elif(e.signal == signals.INIT_SIGNAL):
status = return_status.HANDLED
elif(e.signal == signals.EXIT_SIGNAL):
status = return_status.HANDLED
else:
status = return_status.SUPER
chart.temp.fun = self.c1
return status
We will start up our state chart, in c11 and send the MY_HOOK
event:
ao = ActiveObject()
ao.start_at(c11)
# run code in c1 from c11 by using a hook
ao.post_fifo(Event(signal=signals.MY_HOOK))
# => "running the code from defined in c1"
# demonstrate the state didn't change
assert(ao.state.fun.__name__ == 'c11')
In the above code we see evidence that our statechart ran some application code
contained in the parent state (c1
) while it stayed within its child state
(c11
).
The child state received an event called MY_HOOK
which it didn’t know what
to do with. So the event processor searched the parent state and saw that there was a
handler for this event in c1
. The MY_HOOK
handler (the if-elif
clause) returned return_status.HANDLED
. Upon seeing this value, the event
processor determined that no transition is needed and it stopped running.
In this way hook code is run in the search phase of the search-then-transition part of the event processor algorithm.
The c1
state method, “hooks” the MY_HOOK
event, by capturing it, running
its application code and returning the HANDLED
value. It stops the
MY_HOOK
event from falling off the edge of the map and returns control to
the state that originally experienced the event.
Catch and Release in a State function#
The catch and release recipe is similar to the hook recipe in that you are using the search phase of the event processor algorithm to run your code.
Instead of hooking the code with an HANDLED
response, your state method
returns an UNHANDLED
status. This causes the event processor, to query it
again to find its parent, then dispatch the event to that state method.
Here we create the state in the picture, notice that inner
and middle
do not return HANDLED
when they see the BUBBLED
signal.
def outer(chart, e):
status = return_status.UNHANDLED
if(e.signal == signals.BUBBLED):
print("hooked by the outer state")
status = return_status.HANDLED
else:
status = return_status.SUPER
chart.temp.fun = chart.top
return status
def middle(chart, e):
status = return_status.UNHANDLED
if(e.signal == signals.BUBBLED):
print("processed in middle")
else:
status = return_status.SUPER
chart.temp.fun = outer
return status
def inner(chart, e):
status = return_status.UNHANDLED
if(e.signal == signals.BUBBLED):
print("processed in inner")
else:
status = return_status.SUPER
chart.temp.fun = outer
return status
ao = ActiveObject()
ao.start_at(inner)
# run each state's application code for the bubble event
ao.post_fifo(Event(signal=signals.BUBBLED))
# => "processed in inner"
# "processed in middle"
# "hooked by the outer state"
# demonstrate the state didn't change
assert(ao.state.fun.__name__ == 'inner')
State Method Recipes#
A statechart can be made using the miros.Factory
class. To see how to do
this look here.
Boiler-plate State Method#
A state method is constructed by using the create
, catch
and to_method
interface of the miros.Factory
object.
Consider the outer_state of this example:
Your outer_state state method code (highlighted) would look like this:
class FactoryInstrumentationToLog(Factory):
def __init__(self, name, log_file_name=None,
live_trace=None, live_spy=None):
super().__init__(name)
# ..
self.outer_state = self.create(state="outer_state"). \
catch(signal=signals.ENTRY_SIGNAL,
handler=self.outer_state_entry_signal). \
catch(signal=signals.INIT_SIGNAL,
handler=self.outer_state_init_signal). \
catch(signal=signals.Hook,
handler=self.outer_state_hook). \
catch(signal=signals.Reset,
handler=self.outer_state_reset). \
catch(signal=signals.EXIT_SIGNAL,
handler=self.outer_state_exit_signal). \
to_method()
# ..
The self.outer_state
method construction is started with a call to the
miros.Factory
create
method. Then the catch
method is chained once
for every signal your state needs to handle. The catch
method requires a
signal and a reference to a method which will be called when an event of this
signal is passed to your state method. To end the chain, the to_method
is
called, which converts the state into a state method.
The self.outer_state
method does not describe its parent, this is
done separately with the miros.Factory
’s nest
interface.
Boiler-plate State Handler Method#
When you use the miros.Factory
to build a statechart, you assign one state
handler to each signal of the state. This means that you can have multiple state
handlers per state.
A state method tends to have the following form:
def <name_of_state_name_of_event>(self, e):
status = return_status.HANDLED
# .. code to handle specific event
return status
Here is an example of the outer_state method’s entry event handler taken from this example:
def outer_state_entry_signal(self, e):
self.scribble("hello from outer_state")
status = return_status.HANDLED
return status
Describing your Parent State using the Factory#
A state method does not describe its parent state, this is done with the
nest
method which follows the state definition descriptions in the
__init__
of your derived factory class.
If we were to add the hierarchy information for the following design:
The code would look like this:
self.nest(self.outer_state, parent=None). \
nest(self.inner_state, parent=self.outer_state)
To see the full example, reference: making a statechart from a class.
Passing Events to the Parent State Method#
By default a state method will pass an event outward to its super state if it is not handled by any of its handlers.
Transition to another State Method#
To transition to another state, use the trans
method:
def outer_state_init_signal(self, e):
self.scribble("init")
status = self.trans(self.middle_state)
return status
To see the full example, reference: making a statechart from a class.
State Handlers for Entry/Exit and Initialization Signals#
We would create the outer_state and its entry/exit/initialization handlers this way:
from miros import Event
from miros import signals
from miros import Factory
from miros import return_status
class FactoryInstrumentationToLog(Factory):
def __init__(self, name, log_file_name=None,
live_trace=None, live_spy=None):
# ...
self.outer_state = self.create(state="outer_state"). \
catch(signal=signals.ENTRY_SIGNAL,
handler=self.outer_state_entry_signal). \
catch(signal=signals.INIT_SIGNAL,
handler=self.outer_state_init_signal). \
# ...
catch(signal=signals.EXIT_SIGNAL,
handler=self.outer_state_exit_signal). \
to_method()
# ...
def outer_state_entry_signal(self, e):
self.subscribe(Event(signal=signals.BROADCAST))
self.scribble("hello from outer_state")
status = return_status.HANDLED
return status
def outer_state_init_signal(self, e):
self.scribble("init")
status = self.trans(self.middle_state)
return status
def outer_state_exit_signal(self, e):
status = return_status.HANDLED
self.scribble("exiting the outer_state")
return status
To see the full example, reference: making a statechart from a class.
Creating a Hook in a State Handler#
We would create the outer_state and its Hook handlers this way:
class FactoryInstrumentationToLog(Factory):
def __init__(self, name, log_file_name=None,
live_trace=None, live_spy=None):
super().__init__(name)
# ..
self.outer_state = self.create(state="outer_state"). \
# ..
catch(signal=signals.Hook,
handler=self.outer_state_hook). \
# ..
to_method()
# ..
def outer_state_hook(self, e):
self.scribble("run some code, but don't transition")
return return_status.HANDLED
To see the full example, reference: making a statechart from a class.
Events And Signals#
Creating an Event#
An event is something that will be passed into your statechart, it will be reacted to, then removed from memory.
from miros import Event
from miros import signals
event_1 = Event(signal="name_of_signal")
# or
event_2 = Event(signal=signals.name_of_signal)
Creating a Signal#
A signal is the name of an event. Many different events can have the same name, or signal. When a signal is created, it is given a number which is one higher than the oldest signal that was within your program. You shouldn’t have to worry about what a signal number is, they are only used to speed up the event processor. (it is faster to compare two numbers than two strings)
When you create a signal it will not be removed from memory until your program finishes. They are created at the moment they are referenced, so you don’t have to explicitly define them.
from miros import Event
from miros import signals
# signal named "name_of_signaL" invented
# here and given a unique number
event_1 = Event(signal="name_of_signal")
# the signal number of this event will have
# the same number as in line 6
event_2 = Event(signal=signals.name_of_signal)
Notice that the signal was invented on line 6 then re-used on line 9.
The signals are shared across your whole program. To see reflect upon your signals read this.
Posting Events#
The Active Object post_fifo
, post_lifo
, defer
and recall
methods are use to feed events to the statechart. An Event can be thought of
as a kind of named marble that is placed onto a topological map. If a
particular elevation doesn’t know what to do with the marble, it rolls the
marble to the next lower elevation, or state. If the lowest elevation is
reached and the program doesn’t know what to do, it just ignores the event, or
lets the marble fall out of play.
The name of the marble is the signal name. An event can have a payload, but it doesn’t have to. An event can only be posted to a chart after the chart has started. Otherwise the behavior of the active object is undefined.
The state methods typically react to the names of a event, or the signal names. This means that the if-else structures that you write will use the signal names in their logic.
If you use the chart’s post event methods within the chart, the chart will not concern itself with where you initiated that event. It will post its events into its queue as if they were provided by the outside world. In this way these events are called artificial; instead of the world creating the event, the chart does. There are a number of situations where it makes sense to do this, they will be described in the patterns section.
Posting an Event to the Fifo#
To post an event to the active object first-in-first-out (fifo) buffer, you must have first started your statechart. Here is a simple example:
ao = ActiveObject()
# start at 'outer' for the sake of our example
ao.start_at(outer)
# Send an event with the signal name 'mary'
ao.post_fifo(Event(signal=signals.mary))
The signal names used by the events are common across the entire system. You
do not need to declare them. If the system had not seen the signals.mary
signal code before in our above example, this name would be added and assigned
a unique number automatically.
Posting an Event to the LIFO#
To post an event to the active object last-in-first-out (lifo) buffer, you must have first started your statechart. Here is a simple example:
ao = ActiveObject()
# start at 'outer' for the sake of our example
ao.start_at(outer)
# Now say we want to send an event with
# th the signal name of 'mary' to the chart
ao.post_lifo(Event(signal=signals.mary))
You would post to the ‘lifo’ buffer if you needed your event to be moved to the front of the active object’s collection of unprocessed events. You might want to do this with a timing heart beat or for any event that needs to be processed with a greater priority than other events.
Create a Guard#
There will be situations where you only would like an event to cause a transition between two states if a condition is true. This is called a guard, in UML it looks like this:
The logic between the square brackets must be true for this event to work. In
this case the T
event is guarded, it can only cause a transition if the the
function g()
returns True
, otherwise nothing will happen.
The t()
function is a function that runs if the g()
returns True.
To implement a guard in your state method is very straight forward, you use an if statement:
elif(e.signal == signals.T):
if g():
t()
chart.trans(<state_to_transition_to)
The highlighted code is the guard.
To learn more about guards read the hacking to learn example.
Creating a One-Shot Event#
A one-shot event can be used to add some delay between state transitions. You can think of them as delayed init signals. You might want to use a one-shot if you need a system to settle down a bit before transitioning into an inner state.
Generally speaking, you should cancel your one-shot events as your chart passes control to outer states. You don’t need to do this, but if you don’t your outer states will be hit with one-shot messages that they don’t care about and your chart will needlessly search as it reacts to these events.
It is important to know that if your chart changes state, the event posted to
it will look like it came from outside of your statechart, even though it was
originally generated within a given state. The construction of any event with
the fifo
or lifo
api behaves like this.
# Here define a middle state the creates a one-shot event called
# delayed_one_second. The same delayed_one_second signal is captured
# by the middle state and used to transition into the inner state
@spy_on
def middle(ao, e):
status = state.UNHANDLED
# we have entered the state and we would like to delay one
# second prior to entering the inner state
if(e.signal == signals.ENTRY_SIGNAL):
ao.post_fifo(
Event(signal=signals.delay_one_second),
times=1,
period=1.0,
deferred=True
)
status = state.HANDLED
elif(e.signal == signals.EXIT_SIGNAL):
# we are leaving this state for an outer state
# so we cancel our one-shot in case it hasn't gone off yet
ao.cancel_events(signals.delay_one_second)
status = state.HANDLED
# ignore our init
if(e.signal == signals.INIT_SIGNAL):
status = state.HANDLED
# our one-shot has fired, one second has passed since
# we transitioned into this state, now transition
# to our desired target; 'inner'
elif(e.signal == signals.delay_one_second):
status = ao.trans(inner)
else:
status, ao.temp.fun = state.SUPER, outer
return status
Creating a Multishot Event#
A multi-shot event is just an extension of the one-shot idea. Instead of only being fired once on entry, it can be fired between 2 and an infinite number of times. You would use a multi-shot event if you would like to provide an inner part of your chart with a heart beat that the outer part of your chart doesn’t need to know about. In this way you could save cycles by avoiding unnecessary event processing in the parts of the chart that don’t need these heart beats. This will also be useful while debugging your chart, your logs won’t be filled with unnecessary events.
You should cancel your multi-shot events in the exit handler of the state that created them.
# Here define a middle state the creates a multi-shot event called
# three_pulse. The same three_pulse signal is captured
# by the middle state and used to transition into the inner state
@spy_on
def middle(ao, e):
status = state.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
multi_shot_thread = \
ao.post_fifo(Event(signal=signals.three_pulse),
times=3,
period=1.0,
deferred=True)
# We mark up the ao with this id, so that
# state function can be used by many different aos
ao.augment(other=multi_shot_thread,
name='multi_shot_thread')
status = state.HANDLED
elif(e.signal == signals.EXIT_SIGNAL):
ao.cancel_event(ao.multi_shot_thread)
status = state.HANDLED
if(e.signal == signals.INIT_SIGNAL):
status = state.HANDLED
elif(e.signal == signals.three_pulse):
status = ao.trans(inner)
else:
status, ao.temp.fun = state.SUPER, outer
return status
By setting the times
argument of the post_fifo
to 0, you can create an
infinite multi-shot event. This is how you could make an inner heart beat.
The post_lifo
api can be used the same as the post_fifo
api for
creating these types of repeating events. You would use the post_lifo
api
when you would need your heart beat event signal to barge ahead of all other
events waiting to be processed by the active object.
Canceling a Specific Event Source#
The requests to the post_fifo
and post_lifo
methods, where times
are
specified, can be thought of as event sources. This is because they create
background threads which track time and periodically post events to the active
object.
There are two different ways to cancel event sources. You can cancel a specific event source, or you can cancel all event sources that create a specific signal name (easier). Read the Canceling Event Source By Signal Name recipe to see how to do this.
To cancel a specific signal source, you need to track the thread id which was
created when it was made, then use that id to cancel the event. Since a state
method can be used by many different active objects, you don’t want to store
this id on the method itself, or in its variable name space. Instead, you can
markup the name of the chart that is using the method, this chart
object is
passed to the state method as the first argument.
# Here define a middle state the creates a multi-shot event called
# three_pulse. The same three_pulse signal is captured
# by the middle state and used to transition into the inner state
#
# We want to cancel this specific event source when we are exiting this
# state
@spy_on
def middle(chart, e):
status = state.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
multi_shot_thread = \
chart.post_fifo(Event(signal=signals.three_pulse),
times=3,
period=1.0,
deferred=True)
# We graffiti the provided chart object with this id
chart.augment(other=multi_shot_thread,
name='multi_shot_thread')
status = state.HANDLED
elif(e.signal == signals.EXIT_SIGNAL):
chart.cancel_event(chart.multi_shot_thread)
# remove our graffiti
del(chart.multi_shot_thread)
status = state.HANDLED
if(e.signal == signals.INIT_SIGNAL):
status = state.HANDLED
elif(e.signal == signals.three_pulse):
status = chart.trans(inner)
else:
status, chart.temp.fun = state.SUPER, outer
return status
The augment
api is used to graffiti our chart upon entering the state.
We write the event-source id onto the multi_shot_thread
chart attribute,
so that we can use it later. By marking this specific chart
object, the
middle state method handler can be shared by other active objects.
You would use this method of canceling an event source if you need the
three_pulse signal name elsewhere in your statechart. If you do not intend on
re-using this signal name you can just cancel event sources using a much
simpler api: the cancel_event
.
Canceling Event Source By Signal Name#
If you would like to re-use your event source signal names through your chart,
then you can use the Canceling a Specific Event Source recipe
to cancel a specific source and leave your other event sources running.
Otherwise, you can use the simpler cancel_sources
api provided by the
Active Object:
# Here we define a middle state the creates a multi-shot event called
# three_pulse. The same three_pulse signal is captured
# by the middle state and used to transition into the inner state
#
# We want to cancel this specific event source when we are exiting this
# state
@spy_on
def middle(chart, e):
status = state.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
chart.post_fifo(Event(signal=signals.three_pulse),
times=3,
period=1.0,
deferred=True)
status = state.HANDLED
elif(e.signal == signals.EXIT_SIGNAL):
# cancel all event sources with the signal named three_pulses
chart.cancel_events(Event(signal=signals.three_pulse))
status = state.HANDLED
if(e.signal == signals.INIT_SIGNAL):
status = state.HANDLED
elif(e.signal == signals.three_pulse):
status = chart.trans(inner)
else:
status, chart.temp.fun = state.SUPER, outer
return status
There is no need to keep a thread id for the event source, since the Active
Object can just look at all of the event source threads and kill any of them
that have this signal name provided to the cancel_events
call.
Deferring and Recalling an Event#
There will be situations where you want to post a kind of artificial event into a queue which won’t immediately be acted upon by your statechart. It is an artificial event, because your chart is making it up, it isn’t being given to it by the outside world. It is a way for your chart to build up a kind of processing pressure that can be relieved when you have the cycles to work on things.
This is a two stage process, one, deferring the event, and two, recalling the event. It is called a deferment of an event because we are holding off our reaction to it.
# code to place in the state that is deferring the event:
chart.defer(Event(signal=signals.signal_that_is_deferred)
# code to place in the state where you would like the event reposted into
# the chart's first in first out queue
chart.recall() # posts our deferred event to the chart.
Adding a Payload to an Event#
To add a payload to your event:
e = Event(signal=signals.YOUR_SIGNAL_NAME, payload="My Payload")
If you are creating a payload that will be shared across statecharts put it within an immutable object like a namedtuple before you send it out. Then draw the named tuple onto your diagrams, because the structure of the payload will become extremely important when you are trying to understand your design later.
Note
We want to use an immutable object when sharing data between threads to avoid nasty multi-threading bugs. If you can’t change the object in two different locations at the same time, then you can’t accidently create this kind of bug.
Here is an example of a payload picture, taken from the miros-random project:
You can see, it’s just the code used to make it, placed within a UML note.
Here is the code to make this payload’s class:
# collections are in the Python standard library
from collections import namedtuple
# create a structured immutable object that has useful names related
# to your problem
PioneerRequestSpec = namedtuple(
'PioneerRequestSpec', ['cells_per_generation', 'deque_depth')
Here is how you would create an event with this payload class:
# There is often a relationship between your signal names and your payload
# name
e = Event(signal=signals.PioneerRequest,
payload=PioneerRequestSpec(
cells_per_generation=45,
queue_depth=11)
Here is how you would access the payload elsewhere in your design:
# to get access to the payload information when you receive this event in one
# of your event handlers:
e.payload.cells_per_generation # => 45
e.payload.queue_depth # => 11
I would recommend that you always place your payloads in immutable objects, even if you aren’t intending to share them between statecharts.
Determining if an Event Has a Payload#
To determine if an event has a payload:
e1 = Event(signal=signals.YOUR_SIGNAL_NAME, event="My Payload")
e2 = Event(signal=signals.YOUR_SIGNAL_NAME)
assert(e1.has_payload() == True)
assert(e2.has_payload() == False)
Subscribing to an Event Posted by Another Active Object#
Your active object can subscribe to the events published by other active objects:
subscribing_ao = ActiveObject()
subscribing_ao.subscribe(
Event(signal=signals.THING_SUBSCRIBING_AO_CARES_ABOUT))
An active object can set how the ActiveFabric (the infrastructure connecting all
of your statecharts together) posts events to it. If it would like a message to
take priority over all other events waiting to be managed, you would use the
lifo
technique:
subscribing_ao = ActiveObject()
subscribing_ao.subscribe(
Event(signal=signals.THING_SUBSCRIBING_AO_CARES_ABOUT),
queue_type='lifo')
This approach would make sense if you were subscribed to a timed heart beat being sent out by another active object, or if this event was some sort of safety related thing.
In most situations you can use the subscription defaults:
subscribing_ao = ActiveObject()
subscribing_ao.subscribe(signals.THING_SUBSCRIBING_AO_CARES_ABOUT)
# which is the same as writing
subscribing_ao.subscribe(
signals.THING_SUBSCRIBING_AO_CARES_ABOUT, queue_type='fifo')
It may seem a little bit strange to subscribe to an event, since an event is a
specific thing, which contains a general thing; the signal. But the subscribe
method supports subscribing to events so that its method signature looks like
the other method signatures in the library. (Less things for you to remember)
If you chose to subscribe to events and not directly to signals, think of your call as saying, “I would like to subscribe to this type of event”.
# subscribing to a `type` of event
subscribing_ao.subscribe(
Event(signal=signals.THING_SUBSCRIBING_AO_CARES_ABOUT),
queue_type='fifo')
Publishing events to other Active Objects#
Your active object can send data to other active objects in the system by publishing events.
But your active object can only control how it talks to others, not who listens to it; so, if another active object wants to receive a published event it must subscribe to it first.
If you would like to publish data that will be used by another ActiveObject, copy your data into some sort of immutable object before you publish it: namedtuple objects are perfect for these situations:
from collections import namedtuple
# draw these payloads on your statechart diagram
MyPayload = namedtuple('MyPayload', ['name_of_item_1', 'name_of_item2'])
publishing_ao = ActiveObect()
# This is how you can send an 'THING_SUBSCRIBING_AO_CARES_ABOUT' event
# to anything that has subscribed to it
publishing_ao.publish(
Event(signal=signals.THING_SUBSCRIBING_AO_CARES_ABOUT,
payload=MyPayload(
name_of_item_1='something',
name_of_item_2='something_else'
)
)
)
Here is how to publish an event with a specific priority:
publishing_ao = ActiveObect()
publishing_ao.publish(
Event(signal=signals.THING_SUBSCRIBING_AO_CARES_ABOUT))
# or you can set the priority (1 is the highest priority see note):
publishing_ao.publish(
Event(signal=signals.THING_SUBSCRIBING_AO_CARES_ABOUT),
priority=1)
Note
The priority numbering scheme is counter-intuitive: low numbers mean high priority while high numbers mean low priority. The highest published event priority is 1. By default all published events are given a priority of 1000. If two events have the same priority the queue will behave like a first in first out queue.
Catching Signals Based on Patterns and Tokens#
If you would like to structure your statecharts to catch signal names based on
patterns, or to catch all external signal names reminiscent of Ruby’s
method_missing, or a
glob’s *
pattern, you can do so with miros.
This kind of thing is simple to do, just sub-class the ActiveObject
, write
your matching methods within it and slightly change the structure of your state
functions to use your new methods.
You can find an example of this feature’s need in the SCXML standard. The SCXML standard requires that a statechart
should be able to catch all external signals, like a *
glob, and it
requires that signal catching logic should be able to catch any word within a
.
-tokenized list. For instance, an event handler specified to catch
timeout
would react to a signal called timeout.token1.token2
.
Note
You don’t have to stop with the SCXML standard. You could match your signal names based on regular expressions, or build up your own signal language grammar and express it within your statecharts.
These signal matching requirements are met with the following code (this example is based on an adaptation of test 403 of the SCXML standard):
1# lru_cache can be used to autocache calls (speeds them up)
2from functools import lru_cache
3
4# import required items from the miros library
5from miros import Event
6from miros import spy_on
7from miros import signals
8from miros import ActiveObject
9from miros import return_status
10# ..
11
12# create the "token_match" method which we can use in our
13# state functions
14class MatchableSignalsChart(ActiveObject):
15
16 @lru_cache(maxsize=32)
17 def tockenize(self, signal_name):
18 return set(signal_name.split("."))
19
20 @lru_cache(maxsize=32)
21 def token_match(self, resident, other):
22 other_set = self.tockenize(other)
23 resident_set = self.tockenize(resident)
24 result = True if len(resident_set.intersect(other_set)) >= 1 \
25 else False
26 return result
27
28 # In a state function you can match like a "*" glob or a specific token of a
29 # signal delimited by "."s.
30 @spy_on
31 def s0(self, e):
32 status = return_status.UNHANDLED
33 if(e.signal == signals.ENTRY_SIGNAL):
34 self.post_fifo(Event(signal="timeout.token1.token2"),
35 times=1,
36 period=1.0,
37 deferred=True)
38 status = return_status.HANDLED
39 elif(e.signal == signals.INIT_SIGNAL):
40 status = self.trans(s01)
41 elif(self.token_match(e.signal_name, 'timeout')):
42 status = self.trans(_fail)
43 elif(self.token_match(e.signal_name, "event1")):
44 status = self.trans(_fail)
45 elif(self.token_match(e.signal_name, "event2")):
46 status = self.trans(_pass)
47 else:
48 self.temp.fun = self.top
49 status = return_status.SUPER
50 return status
51
52 @spy_on
53 def s01(self, e):
54 status = return_status.UNHANDLED
55 if(e.signal == signals.ENTRY_SIGNAL):
56 self.post_fifo(Event(signals="token3.event1.token4"))
57 status = return_status.HANDLED
58 elif(self.token_match(e.signal_name, "event1")):
59 status = self.trans(s02)
60 elif(signals.is_inner_signal(e.signal)):
61 self.temp.fun = s0
62 status = return_status.SUPER
63 else: # "*" catch any other other external signal
64 status = self.trans(_fail)
65 return status
66
67 # ..
68 # other state functions
69 # ..
70
71 if __name__ == "__main__"
72 ao = MatchableSignalsChart("example")
73 ao.start_at(s0)
74 ao.post_fifo(Event(signals=signals.event4))
I have highlighted the interesting parts of the example.
On line 2 lru_cache
is imported from the standard library. This decorator
allows functions to auto-cache their results. To begin with the cache is empty.
When the function is called the first time, it calculates the result, then
caches the input/output pair. The next time that input is seen, it will just
looks up output from the cache. So lru_cache
can radically speed up calls
to the function it is decorating.
The lru_cache
decorator is used on the tokenize
and token_match
methods of the MatchableSignalsChart
subclass of the ActiveObject
. The
tokenize
method returns a set of tokens from a signal_name delimited by a
.
character. The token_match
method returns true if there is an
intersection of tokens in two strings. You can see how this method is called on
line 58.
An example of how to build a tokenized signal can be seen on line 34.
Lines 60 through 64 show how to implement a “*”, catch-all kind of signal
handler. The elif
clause catching internal signals on line 60, allow the
event processor to search the function using its internal signals. Any external
signals which aren’t already managed between line 53 and 62 is caught by the
else
clause on line 63. In this way the else
clause acts as the
catch-all (*
), part of the state function; any external signal that does
not match the event1
token will cause a transition into the _fail
state.
Avoiding Bugs Which Travel Through Time#
If you have build a statechart which is dependent upon an internal heartbeat to
track time, you have built a clock. Such a clock will slip backward in time
relative to your Operating System’s clock. This is because the threads which
are created to drive your heart beat use Python’s time.sleep
function, and it
is not precise in how it works. A call to time.sleep
means it will sleep
for at-least a given amount of time, not the precise number giving to it as
an argument.
If you mix another clock into your design, by calling out to get the your operating system’s version of time, and mix that information with the time you have calculated from your heart beat, you will have created a time traveling bug. One time measurement will be in the future of the others time reference.
This relative time slippage is non-deterministic because time.sleep
is
dependent upon what your computer is doing outside of your program. The time
distortion will get larger the longer your program runs.
To avoid such issues, only use one clock reference per statechart. If you have constructed a federation of statecharts which need to have their time synchronized, then use one time reference for the federation.
Avoiding double time heart beat bugs#
If you are seeing more heart beats than you want, look to see if you have designed in the double start bug or the heart-beat-bleed bug.
Avoiding Heart-Beat-Bleed Bugs#
When you create a heart beat in an entry condition to a state, you will want to turn it off in the exit condition of the same state. If you don’t, the thread producing its events will continue to run and post events to your chart. This may not be an issue outside of the state that is reacting to these events, but once you re-enter the state and re-create the heart beat, you will now have two threads sending beats into that part of your system.
You can find such bugs by monitoring the spy output of your statechart, or by remembering that every time you create a heart beat, you must cancel it in the exit condition.
ActiveObjects#
To build a statechart, you can create an ActiveObject
and connect it to one of
your state functions using the start_at
method. Together, the
ActiveObject
and the state functions work as a statechart.
Starting an ActiveObject#
Once you have created an Activeobject you can start its statemachine and thread
with its start_at
method.
There is a set of queues and threads which connect all of your ActiveObjects
together (the ActiveFabric), if it hasn’t been started yet, the start_at
method will turn on this infrastructure as well.
Here is a simple example:
The start_at
method can start the statechart in any of its states.
Here is the code:
import time
from miros import spy_on
from miros import ActiveObject
from miros import signals, Event, return_status
@spy_on
def c(chart, e):
status = return_status.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
status = return_status.HANDLED
print("c1 entered")
elif(e.signal == signals.INIT_SIGNAL):
status = chart.trans(c1)
elif(e.signal == signals.B):
status = chart.trans(c)
else:
chart.temp.fun = chart.top
status = return_status.SUPER
return status
@spy_on
def c1(chart, e):
status = return_status.UNHANDLED
if(e.signal == signals.A):
status = chart.trans(c2)
else:
chart.temp.fun = c
status = return_status.SUPER
return status
@spy_on
def c2(chart, e):
status = return_status.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
print("c2 entered")
status = return_status.HANDLED
elif(e.signal == signals.A):
status = chart.trans(c1)
else:
chart.temp.fun = c
status = return_status.SUPER
return status
if __name__ == "__main__":
ao = ActiveObject('start_example')
print("calling: start_at(c2)")
ao.start_at(c2)
time.sleep(0.2)
print(ao.trace()) # print what happened from the start_at call
ao.clear_trace() # clear our instrumentation
print("sending B, then A, then A:")
ao.post_fifo(Event(signal=signals.B))
ao.post_fifo(Event(signal=signals.A))
ao.post_fifo(Event(signal=signals.A))
time.sleep(0.2)
print(ao.trace()) # print what happened
When we run this code we will see this result:
calling: start_at(c2)
c1 entered
c2 entered
[2019-06-21 06:05:36.234137] [start_example] e->start_at() top->c2
sending B, then A, then A
c1 entered
c2 entered
[2019-06-21 06:05:36.435853] [start_example] e->B() c2->c1
[2019-06-21 06:05:36.436074] [start_example] e->A() c1->c2
[2019-06-21 06:05:36.436228] [start_example] e->A() c2->c1
Stopping an ActiveObject#
If you would like to stop an ActiveObject
you can use its stop
method.
This will stop its thread, and it will stop all of that ActiveObject
’s slave
threads (constructed by the post_fifo or post_lifo heartbeat constructors). The
stop
method sets the ActiveObject
’s ActiveFabric-facing queue to None, so that
the ActiveFabric will not post items to it anymore.
Note
Calling the stop
method will not stop the ActiveFabric. But the
ActiveFabric, like all threads in miros, is a daemonic thread, so it will stop
running when your program has stopped running.
Augmentng your ActiveObject#
It is a bad idea to add variables to the state methods, instead augment your
active objects using the augment
command.
chart = ActiveObect()
chart.augment(other=0, name='counter')
assert(chart.counter == 0)
Note
An even better idea would be to include the attributes in a subclass of an Activeobject or Factory.
Factories#
You can build a statechart within a class by using the miros.Factory
class.
The miros.Factory
lets you build state methods, state handlers and start the
chart in whichever state you need.
Creating a Statechart Inside of a Class#
You can create a class that has a statechart within it. Here are some of the benefits of programming this way:
it is easy to draw using a mixture of class and statechart UML
state names can be re-used in the same file
provides a clear synchronous interface (methods)
provides a clear asynchronous interface (post_fifo/post_lifo/publish/subscribe)
packages all of your states, transitions and starting code within a single location in your file, within its class.
the start_at code is held within the class’s
__init__
methodhides the state complexity from the rest of your code base
effortlessly provides multi-threading without its dangers
complicated
else
clauses in state callback handlers are avoidedit is trivial for main to inject asynchronous information
it is not hard for main to extract asynchronous information
it is easy to build lots of these different kinds of objects and have them work as a federation. (systems programming)
it is easy to document federations (systems design)
it is not hard to network your federations with other federations across the internet (systems of systems: miros-rabbitmq)
To program this way we use the Factory class from miros.
Here is a simple example of a statechart within an object:
Familiar stuff first: The ClassWithStatechartInIt
inherits from
Factory
, it has three attributes and two methods.
The ClassWithStatechartInIt
also has an asynchronous statechart (blue),
which is attached to an event processor, and it starts in the
common_behaviors
state.
Let’s bring this design to life with some code (we will highlight the asynchronous aspects of the program):
1import time
2
3from miros import Event
4from miros import signals
5from miros import Factory
6from miros import return_status
7
8class ClassWithStatechartInIt(Factory):
9 Default_Name = 'default_name'
10 def __init__(self, name=None, live_trace=None, live_spy=None):
11 # call the Factory ctor
12 super().__init__(
13 ClassWithStatechartInIt.Default_Name if name == None else name
14 )
15 # determine how this object will be instrumented
16 self.live_spy = False if live_spy == None else live_spy
17 self.live_trace = False if live_trace == None else live_trace
18
19 # define our states and their statehandlers
20 self.common_behaviors = self.create(state="common_behaviors"). \
21 catch(signal=signals.INIT_SIGNAL,
22 handler=self.common_behaviors_init). \
23 catch(signal=signals.hook_1,
24 handler=self.common_behaviors_hook_1). \
25 catch(signal=signals.hook_2,
26 handler=self.common_behaviors_hook_2). \
27 catch(signal=signals.reset,
28 handler=self.common_behaviors_reset). \
29 to_method()
30
31 self.a1 = self.create(state="a1"). \
32 catch(signal=signals.ENTRY_SIGNAL,
33 handler=self.a1_entry). \
34 catch(signal=signals.to_b1,
35 handler=self.a1_to_b1). \
36 to_method()
37
38 self.b1 = self.create(state="b1"). \
39 catch(signal=signals.INIT_SIGNAL,
40 handler=self.b1_init). \
41 catch(signal=signals.ENTRY_SIGNAL,
42 handler=self.b1_entry). \
43 catch(signal=signals.EXIT_SIGNAL,
44 handler=self.b1_exit). \
45 to_method()
46
47 self.b11 = self.create(state="b11"). \
48 to_method()
49
50 # nest our states within other states
51 self.nest(self.common_behaviors, parent=None). \
52 nest(self.a1, parent=self.common_behaviors). \
53 nest(self.b1, parent=self.common_behaviors). \
54 nest(self.b11, parent=self.b1)
55
56 def start(self):
57 # start our statechart, which will start its thread
58 self.start_at(self.common_behaviors)
59 # give your thread a moment to start and climb into
60 # the appropriate state prior to handing back control
61 # to our client code
62 time.sleep(0.001)
63 return self
64
65 def common_behaviors_init(self, e):
66 status = self.trans(self.a1)
67 return status
68
69 def common_behaviors_hook_1(self, e):
70 status = return_status.HANDLED
71 # call the ClassWithStatechartInIt work2 method
72 self.worker1()
73 return status
74
75 def common_behaviors_hook_2(self, e):
76 status = return_status.HANDLED
77 # call the ClassWithStatechartInIt work2 method
78 self.worker2()
79 return status
80
81 def common_behaviors_reset(self, e):
82 status = self.trans(self.common_behaviors)
83 return status
84
85 def a1_entry(self, e):
86 status = return_status.HANDLED
87 # post an event to ourselves
88 self.post_fifo(Event(signal=signals.to_b1))
89 return status
90
91 def a1_to_b1(self, e):
92 status = self.trans(self.b1)
93 return status
94
95 def b1_init(self, e):
96 status = return_status.HANDLED
97 return status
98
99 def b1_entry(self, e):
100 status = return_status.HANDLED
101 # post an event to ourselves
102 self.post_fifo(Event(signal=signals.hook_1))
103 return status
104
105 def b1_exit(self, e):
106 status = return_status.HANDLED
107 # post an event to ourselves
108 self.post_fifo(Event(signal=signals.hook_2))
109 return status
110
111 def worker1(self):
112 print('worker1 called')
113
114 def worker2(self):
115 print('worker2 called')
116
117if __name__ == '__main__':
118 chart = ClassWithStatechartInIt(name='chart', live_trace=True).start()
119 chart.post_fifo(Event(signal=signals.reset))
120 time.sleep(1)
This will result in the following output:
[2019-06-19 06:16:02.662672] [chart] e->start_at() top->a1
[2019-06-19 06:16:02.662869] [chart] e->to_b1() a1->b1
worker1 called
[2019-06-19 06:16:02.664588] [chart] e->reset() b1->a1
worker2 called
[2019-06-19 06:16:02.665011] [chart] e->to_b1() a1->b1
worker1 called
Note
To see why we start the statechart this way read avoiding the double start bug.
Here is something a bit weirder, a concurrent statechart:
Above we define a class that contains a statechart that subscribes to, and publishes events to other statecharts. The class will be used to create three objects which will message each other.
Upon starting, there is a 2/5 chance the statechart within
ClassWithStatechartInIt
will end up within b11
state. If a chart ends
up in this state, it will publish the OTHER_INNER_MOST
signal to any chart
that has subscribed to the signal_name.
The chart sending the OTHER_INNER_MOST
event ignores it, and all other
charts will respond by re-entering their common_behaviors
state if they are
not in the b11
state.
Note
The red and green dots are not UML. They are markers that act to highlight the important parts of a concurrent statechart design.
I put the red dot on the part of the chart that is publishing an event. It is red because once an item is published, it is put in a queue and the message flows stops momentarily.
I put the green dot beside events that have been subscribed to and have been posted to the chart. They are green, because they have been extracted from a queue by a thread and are being posted to the event processor attached to the chart.
We will make three of these charts, turn on some instrumentation, run them in parallel and see what happens.
Here is the code (asynchronous parts highlighted):
1import time
2import random
3
4from miros import Event
5from miros import signals
6from miros import Factory
7from miros import return_status
8
9class ClassWithStatechartInIt(Factory):
10 def __init__(self, name, live_trace=None, live_spy=None):
11
12 # call the Factory ctor
13 super().__init__(name)
14
15 # determine how this object will be instrumented
16 self.live_spy = False if live_spy == None else live_spy
17 self.live_trace = False if live_trace == None else live_trace
18
19 # define our states and their statehandlers
20 self.common_behaviors = self.create(state="common_behaviors"). \
21 catch(signal=signals.INIT_SIGNAL,
22 handler=self.common_behaviors_init). \
23 catch(signal=signals.ENTRY_SIGNAL,
24 handler=self.common_behaviors_entry). \
25 catch(signal=signals.hook_1,
26 handler=self.common_behaviors_hook_1). \
27 catch(signal=signals.hook_2,
28 handler=self.common_behaviors_hook_2). \
29 catch(signal=signals.reset,
30 handler=self.common_behaviors_reset). \
31 catch(signal=signals.OTHER_INNER_MOST,
32 handler=self.common_behaviors_other_inner_most). \
33 to_method()
34
35 self.a1 = self.create(state="a1"). \
36 catch(signal=signals.ENTRY_SIGNAL,
37 handler=self.a1_entry). \
38 catch(signal=signals.to_b1,
39 handler=self.a1_to_b1). \
40 to_method()
41
42 self.b1 = self.create(state="b1"). \
43 catch(signal=signals.INIT_SIGNAL,
44 handler=self.b1_init). \
45 catch(signal=signals.ENTRY_SIGNAL,
46 handler=self.b1_entry). \
47 catch(signal=signals.EXIT_SIGNAL,
48 handler=self.b1_exit). \
49 to_method()
50
51 self.b11 = self.create(state="b11"). \
52 catch(signal=signals.ENTRY_SIGNAL,
53 handler=self.b11_entry). \
54 catch(signal=signals.inner_most,
55 handler=self.b11_inner_most). \
56 catch(signal=signals.OTHER_INNER_MOST,
57 handler=self.b11_other_inner_most). \
58 to_method()
59
60 # nest our states within other states
61 self.nest(self.common_behaviors, parent=None). \
62 nest(self.a1, parent=self.common_behaviors). \
63 nest(self.b1, parent=self.common_behaviors). \
64 nest(self.b11, parent=self.b1)
65
66 def start(self):
67 # start our statechart, which will start its thread
68 self.start_at(self.common_behaviors)
69 # give your thread a moment to start and climb into
70 # the appropriate state prior to handing back control
71 # to our client code
72 time.sleep(0.001)
73 return self
74
75 def common_behaviors_init(self, e):
76 status = self.trans(self.a1)
77 return status
78
79 def common_behaviors_entry(self, e):
80 status = return_status.HANDLED
81 self.subscribe(Event(signal=signals.OTHER_INNER_MOST))
82 return status
83
84 def common_behaviors_hook_1(self, e):
85 status = return_status.HANDLED
86 # call the ClassWithStatechartInIt work2 method
87 self.worker1()
88 return status
89
90 def common_behaviors_hook_2(self, e):
91 status = return_status.HANDLED
92 # call the ClassWithStatechartInIt work2 method
93 self.worker2()
94 return status
95
96 def common_behaviors_reset(self, e):
97 status = self.trans(self.common_behaviors)
98 return status
99
100 def common_behaviors_other_inner_most(self, e):
101 status = self.trans(self.b11)
102 return status
103
104 def a1_entry(self, e):
105 status = return_status.HANDLED
106 # post an event to ourselves 2/5 of the time
107 if random.randint(1, 5) <= 3:
108 self.post_fifo(Event(signal=signals.to_b1))
109 return status
110
111 def a1_to_b1(self, e):
112 status = self.trans(self.b1)
113 return status
114
115 def b1_init(self, e):
116 status = self.trans(self.b11)
117 return status
118
119 def b1_entry(self, e):
120 status = return_status.HANDLED
121 # post an event to ourselves
122 self.post_fifo(Event(signal=signals.hook_1))
123 return status
124
125 def b1_exit(self, e):
126 status = return_status.HANDLED
127 # post an event to ourselves
128 self.post_fifo(Event(signal=signals.hook_2))
129 return status
130
131 def b11_entry(self, e):
132 status = return_status.HANDLED
133 self.post_fifo(Event(signal=signals.inner_most))
134 return status
135
136 def b11_inner_most(self, e):
137 status = return_status.HANDLED
138 self.publish(Event(signal=signals.OTHER_INNER_MOST))
139 return status
140
141 def b11_other_inner_most(self, e):
142 status = return_status.HANDLED
143 return status
144
145 def worker1(self):
146 print('{} worker1 called'.format(self.name))
147
148 def worker2(self):
149 print('{} worker2 called'.format(self.name))
150
151if __name__ == '__main__':
152 chart1 = ClassWithStatechartInIt(name='chart1', live_trace=True).start()
153 chart2 = ClassWithStatechartInIt(name='chart2', live_trace=True).start()
154 chart3 = ClassWithStatechartInIt(name='chart3', live_trace=True).start()
155 # send a reset event to chart1
156 chart1.post_fifo(Event(signal=signals.reset))
157 time.sleep(0.2)
There is a probabilistic aspect to this program, so it could behave differently every time you run it. Here is what I saw when I ran it for the first time:
[2019-06-19 12:03:17.949042] [chart1] e->start_at() top->a1
[2019-06-19 12:03:17.949227] [chart1] e->to_b1() a1->b11
chart1 worker1 called
[2019-06-19 12:03:17.962356] [chart2] e->start_at() top->a1
[2019-06-19 12:03:17.962482] [chart2] e->to_b1() a1->b11
chart2 worker1 called
[2019-06-19 12:03:17.975225] [chart3] e->start_at() top->a1
[2019-06-19 12:03:17.985577] [chart1] e->reset() b11->a1
chart1 worker2 called
[2019-06-19 12:03:17.986041] [chart1] e->to_b1() a1->b11
chart1 worker1 called
[2019-06-19 12:03:17.986845] [chart3] e->OTHER_INNER_MOST() a1->b11
chart3 worker1 called
The diagram describing this concurrent statechart is very compact and detailed, but we may want an even smaller version which hides the specifics of how the statechart behaves.
Typically class diagrams are suppose to describe the attributes and the
messages
(methods) which can be received by an object instantiated from the
class. When you pack a statechart into a class, the idea of a message breaks
into two things:
synchronous messages (methods), and
asynchronous messages (events)
The asynchronous messages should be more nuanced: broken into events intended only for one statechart (using post_fifo/post_lifo) and events that are intended to be published into other statecharts which have subscribed to their signal names.
As far as I know there is no standard way of describing how to do this, so I’ll show you how I do it:
We see that the ClassWithStatechartInIt
state inherits from the Factory
and that it has three attributes: name
, live_spy
and live_trace
. It
has two methods, worker1
and worker2
.
The rest of the diagram is non-standard: I put an e
in front of the
internal (e)vents from the chart, the to_b1
and the reset
then I mark the
asynchronous interface, by placing red and green dots on the compact form of a
state icon. Then place the signal names beside arrows showing how they publish or
subscribe to these signal names.
UML isn’t descriptive enough to actually capture a design’s intention, so I never hesitate to mark up a diagram with code. In this case, my code is saying, let’s make three of these things and run them together.
As you become more experienced building statecharts that work in concert, you will notice that you stop paying any attention to what attributes or methods a specific class has. You don’t care about its internal events and you don’t care about from what it was inherited. You only care about what published events it consumes and what events it publishes:
Then to draw a federation:
There is probably a much better way to do this, since it looks like three classes are working together rather than three instantiated objects from the same class. UML really falls-over in describing object interactions.
If you have any suggestions about how to draw this better, email me.
Inheritance and Starting (Factories)#
If you write the start_at
call inside of the __init__
method of a
Factory object which you are intending to overload using inheritance, you are in
for a world of trouble-shooting pain. Your inherited class will inadvertently
start its parent active object, your heart-beat events will be duplicated and
your chart will appear to run (n+1)X as fast, where n is the number of inheritance
actions you have taken on your factory-derived class.
class Charger(Factory):
def __init__(self, name, live_trace, live_spy):
super().__init__(name)
# define attributes ...
# define states ...
# nest states ...
# start the chart (making a debugging time-bomb by having this in the
# __init__ method)
self.start_at(<state_name>) # starts a thread
class ChargerChild(Charger):
def __init__(self, name, live_trace, live_spy):
# this will construct the parents state machine and start it
super().__init__(name, live_trace, live_spy)
# overload attributes ...
# overload states ...
# nest states ...
# start the chart, BUT IT IS ALREADY RUNNING!
self.start_at(<state_name>) # starts another thread
if __name__ == "__main__":
# This object has two threads running its active object, so any
# heart beat will be running twice as fast and along with other weird
# multithreading race bugs.
charger_child = ChargerChild(
name="charger", live_trace=False, live_spy=False
)
To avoid such an issue, you can explicitly call the start_at
method with the
state you would like your statechart to start in outside of the __init__
call. But, if you do this state_at
call is made entirely outside of the
Factory, you will no longer be data-hiding its state information from the client
code, (which defeats one of the reasons to use a Factory in the first place).
class Charger(Factory):
def __init__(self, name, live_trace, live_spy):
super().__init__(name)
# define attributes ...
# define states ...
# nest states ...
class ChargerChild(Charger):
def __init__(self, name, live_trace, live_spy):
# this will construct the parents state machine and start it
super().__init__(name, live_trace, live_spy)
# overload attributes ...
# overload states ...
# nest states ...
if __name__ == "__main__":
charger_child = ChargerChild(
name="charger", live_trace=False, live_spy=False
)
# Create a thread and start the object in the <state_name> state
# (but I don't want to have to know how the charger_child object works.
# The Charger knows where it needs to start, but I don't want it to start
# itself in the __init__ state (for the reasons described above).
charger_child.start_at(charger_child.<state_name>)
Instead, create a start
method which returns self. This way you can create
and start an active object in one chained-call, hide state from the client and
still have the option of opening up the active object for re-writes through
inheritance without accidently starting parent renegade threads.
class Charger(Factory):
def __init__(self, name, live_trace, live_spy):
super().__init__(name)
# define attributes ...
# define states ...
# nest states ...
def start(self):
self.start_at(self.<state_name>)
return self
class ChargerChild(Charger):
def __init__(self, name, live_trace, live_spy):
# this will construct the parents state machine and start it
super().__init__(name, live_trace, live_spy)
# overload attributes ...
# overload states ...
# nest states ...
# start the chart, BUT IT IS ALREADY RUNNING!
if __name__ == "__main__":
# Initialize the ChargerChild statechart and start it, the starting state
# is hidden within the obj, so as a client I don't have to know how its
# inner state machine works.
charger_child = ChargerChild(
name="charger", live_trace=False, live_spy=False
).start()
The added benefit of this approach is that the subclass can start the object in
a different initial state by overloading the start
method.
Multiple Statecharts#
Break your design down into different interacting charts by using the ActiveFabric.
The active fabric is a set of background tasks which act together to dispatch events between the active objects in your system.
As a user of the software you wouldn’t touch the active fabric directly, but
instead would use your active object’s publish
and subscribe
methods. The
active fabric has two different priority queues and two different tasks which
pend upon them. One is for managing subscriptions in a first in first (fifo)
out manner, the other is for managing messages in a last in first out (lifo)
manner. By having two different queues and two different tasks an active
object is given the option to subscribe to another active object’s published
events, using one of two different strategies:
If it subscribes to an event using the
fifo
strategy, the active fabric will post events to it using its post_fifo method.If it subscribes in a
lifo
way it will post using the post_lifo method.
You can also set the priority of the event while it is in transit within the active fabric. This would only be useful if you are expecting contention between various events being dispatched across your system.
Building and Destroying Workers#
If you have a long running task that has to access blocking IO or connect to a slow API, you can wrap it up into a worker. Unlike stateless architectures, with miros you can make your workers as complicated as you want, sending them events as they sit or pend; change their behaviors in response to unexpected things, or even have rich communications with them prior to having them destroy themselves.
A worker can be thought of as some code sitting in a parallel thread, which will do work, then post the results of this work back to their federation before they prepare themselves for garbage collection.
Seeing What is Going On#
Thread-Safe Printing and Instrumentation#
Anytime you create and start an ActiveObject
you create a thread. The
print
function in python is not thread safe, but the ActiveObject
provides a thread-safe print
method. You can call it like this:
@spy_on
def some_state(self, e):
status = return_state.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
# thread safe print
self.print('writing that we are entering some_state')
status = return_status.HANDLED
else:
self.temp = self.top
status = return_status.SUPER
return status
As of miros 4.2.1
the live_trace
and live_spy
features are run
through-thread safe versions of print. If you over-write their mechanisms using
the register_live_spy_callback
or register_live_trace_callback
interface, whatever function you provide will behave in a thread safe manner.
Determining the Current State#
To see what state your chart is in:
# state name as a string
chart.state_name
# state as a function
chart.state_fn
Note
This will only work if you have wrapped your statemethod with a @spy_on
decorator or if you have constructed your statechart with the Factory class.
Seeing what Signals You Have In Your System#
A signal is the name that can be given to an event. To get access to your signals:
from miros import signals
The signals object is provided by a singleton of the SignalSource class, which
is just an OrderedDictionary with a __getattr__
method to make the syntax
easier to use.
This basically means that you can think of the signals object as being a dict that is shared across your whole program.
To see your signals, you just reflect upon it like you would with any other Python dictionary:
# To see your signal names:
signal_names = signals.keys()
# To see your signal numbers:
signal_numbers = signal.values()
# To output your names and number:
for signal_name, signal_number in signals.items():
print(signal_name, signal_number)
# same output with some formatting
max_name_len = len(max(signals, key=len))
max_number_len = len(str(max(signals.values(), key=int)))
for signal_name, signal_number in signals.items():
print("{1: <{0}} {2:{3}}".format(max_name_len,
signal_name,
signal_number,
max_number_len)) # output below ->
# ENTRY_SIGNAL 1
# EXIT_SIGNAL 2
# INIT_SIGNAL 3
# REFLECTION_SIGNAL 4
# SEARCH_FOR_SUPER_SIGNAL 5
# ..
To compare a received event against a signal, compare the signal numbers:
def some_example_state(chart, e):
status = return_status.UNHANLDED
if(e.signal == signals.ENTRY_SIGNAL):
# do something
It you wanted to read an event’s signals name as a string, you would call the
signal_name
method of the Event class:
def some_example_state(chart, e):
status = return_status.UNHANLDED
print(e.signal_name) # "ENTRY_SIGNAL"
If you have a signal number and you want to determine its name:
signal_name = signals.name_for_signal(1) # ENTRY_SIGNAL
signal_name = signals.name_for_signal(signals.ENTRY_SIGNAL) # ENTRY_SIGNAL
Using the Trace#
If you would like to see what your active object was doing from a very high level, you can look at its trace instrumentation. The trace will only create a log item if a state transition has occurred. Each line in the trace will contain:
A datetime stamp between square brackets
The active object name, between square brackets
The event that caused the transition, its signal number and its payload
The starting state
The ending state
If you haven’t named your active object, a unique identifier is given to it, and the first 5 characters of this unique identifier will be used in the trace. The reason that an identifier is given to it is so that the trace outputs, from multiple active objects, can be distinguished from one another.
Suppose you have built the tazor active object described in the second example. Suppose you named this active object tazor: To see the trace you would type:
tazor.trace()
This would output the following trace:
[2017-11-06 08:34:28.268873] [75c8c] e->start_at() top->arming
[2017-11-06 08:34:26.312241] [75c8c] e->BATTERY_CHARGE() arming->armed
[2017-11-06 08:34:26.312241] [75c8c] e->BATTERY_CHARGE() armed->armed
[2017-11-06 08:34:26.312241] [75c8c] e->BATTERY_CHARGE() armed->armed
Notice that on line one, the signal is called start_at. There is no signal called start_at, here the trace is actually using the method name start_at to indicate how the chart was started.
A trace can be used with the sequence project to generate a sequence diagram, more about that is described here.
Using the Spy#
If you need a very detailed description of your system’s behavior you will want to use the spy instrumentation. The spy outputs:
All of the internal activity that is run by the event processor
How your chart reacts to the events it has received
Information about what happened between each of the rtc activities.
If a signal was hooked by a state
If and when a signal was deferred
If and when a signal was recalled
The number of events awaiting immediate processing after a specific rtc activity.
The number of events which have had their processing deferred.
If you don’t understand what these terms mean, read the simple posting example, since it introduces all of these ideas.
To access the full spy:
# import a pretty printer (like print)
from miros import pp
# .. state charts and other code here
# Assuming your active object is called `ao`
# we use the pretty printer to write the
# full spy log to the terminal
pp(ao.spy())
This might output:
['START',
'SEARCH_FOR_SUPER_SIGNAL:middle',
'SEARCH_FOR_SUPER_SIGNAL:outer',
'ENTRY_SIGNAL:outer',
'ENTRY_SIGNAL:middle',
'INIT_SIGNAL:middle',
'<- Queued:(0) Deferred:(0)',
'A:middle',
'SEARCH_FOR_SUPER_SIGNAL:inner',
'ENTRY_SIGNAL:inner',
'POST_DEFERRED:B',
'INIT_SIGNAL:inner',
'<- Queued:(0) Deferred:(1)',
'A:inner',
'A:middle',
'EXIT_SIGNAL:inner',
'SEARCH_FOR_SUPER_SIGNAL:inner',
'ENTRY_SIGNAL:inner',
'POST_DEFERRED:B',
'INIT_SIGNAL:inner',
'<- Queued:(0) Deferred:(2)',
'A:inner',
'A:middle',
'EXIT_SIGNAL:inner',
'SEARCH_FOR_SUPER_SIGNAL:inner',
'ENTRY_SIGNAL:inner',
'POST_DEFERRED:B',
'INIT_SIGNAL:inner',
'<- Queued:(0) Deferred:(3)',
'D:inner',
'D:middle',
'D:outer',
'POST_FIFO:B',
'D:outer:HOOK',
'<- Queued:(1) Deferred:(2)',
'B:inner',
'B:middle',
'B:outer',
'EXIT_SIGNAL:inner',
'EXIT_SIGNAL:middle',
'EXIT_SIGNAL:outer',
'ENTRY_SIGNAL:outer',
'POST_FIFO:B',
'RECALL:B',
'INIT_SIGNAL:outer',
'<- Queued:(1) Deferred:(1)',
'B:outer',
'EXIT_SIGNAL:outer',
'ENTRY_SIGNAL:outer',
'POST_FIFO:B',
'RECALL:B',
'INIT_SIGNAL:outer',
'<- Queued:(1) Deferred:(0)',
'B:outer',
'EXIT_SIGNAL:outer',
'ENTRY_SIGNAL:outer',
'INIT_SIGNAL:outer',
'<- Queued:(0) Deferred:(0)']
If you would like to understand in detail how to read this log, and how it might have occurred, reference the example from which it came.
The spy log is a ring buffer that contains 500 spots. This is so your system can run forever without using an infinite amount of memory. Once the internal spy log has run out of room, it will shift out the old data and write the new data at the tail end of its log.
Add Timing Information to the Spy#
The spy doesn’t contain timing information, if you would like to mark it up with time so that you can compare it with the trace output:
from datetime import datetime
# .
# .
# In your state method or callback code write:
chart.scribble("{} at {}". \
format(e.signal_name, datetime.now().strftime("%M:%S:%f")))
Tracing Live#
There are situations where you would like to see what an active object is doing
while it is running. Each active object has an attribute called
live_trace
. By setting this attribute to True
the active object will
output its trace information to the terminal while it reacts to events:
To turn on/off the live trace:
ao1 = ActiveObject()
# turn on the live trace
ao1.live_trace = True
# your code and state interactions here
# live trace information will be displayed on the terminal
# turn off the live trace
ao1.live_trace = False
Spying Live#
There are situations where you would like to see what an active object is doing
while it is running. Each active object has an attribute called
live_spy
. By setting this attribute to True
the active object will
output its spy information to the terminal while it reacts to events:
To turn on/off the live spy:
ao1 = ActiveObject()
# turn on the live spy
ao1.live_spy = True
# your code and state interactions here
# live spy information will be displayed on the terminal
# turn off the live spy
ao1.live_spy = False
Scribble On the Spy#
To add messaging to your spy log so that you can see how an activity is
situated within the statechart’s behavior, use the scribble
api within your
state method:
@spy_on
def s2_state(chart, e):
def c(chart):
chart.scribble("running c()")
def d(chart):
chart.scribble("running d()")
status = return_status.UNHANDLED
if(e.signal == signals.ENTRY_SIGNAL):
c(chart)
status = return_status.HANDLED
if(e.signal == signals.INIT_SIGNAL):
d(chart)
status = chart.trans(s21_state)
elif(e.signal == signals.EXIT_SIGNAL):
status = return_status.HANDLED
else:
status, chart.temp.fun = return_status.SUPER, s_state
return status
Flatting a State Method#
If you have created a state method using either a template
or a Factory
and you would like to see its code as if it where written by hand, use the
to_code
call.
Let’s first look how to flatten a template state method:
from miros import state_method_template
from miros import ActiveObject
from miros import signals, Event, return_status
def trans_to_fc1(chart, e):
return chart.trans(fc1)
# create a state method using a template
fc = state_method_template('fc')
# build an active object, which has an event processor
ao = ActiveObject()
# write the design information into the fc state method
ao.register_signal_callback(fc, signals.BB, trans_to_fc)
ao.register_signal_callback(fc, signals.INIT_SIGNAL, trans_to_fc1)
ao.register_parent(fc, ao.top)
# to see how fc would be written as a flat method:
print(ao.to_code(fc)) # ->
# @spy_on
# def fc(chart, e):
# status = return_status.UNHANDLED
# if(e.signal == signals.ENTRY_SIGNAL):
# status = return_status.HANDLED
# elif(e.signal == signals.INIT_SIGNAL):
# status = trans_to_fc1(chart, e)
# elif(e.signal == signals.BB):
# status = trans_to_fc(chart, e)
# elif(e.signal == signals.EXIT_SIGNAL):
# status = return_status.HANDLED
# else:
# status, chart.temp.fun = return_status.SUPER, chart.top
# return status
Above we see that the state method is flattened using the to_code
method of
the active object. You could copy this and drop it into your design for
debugging purposes.
The same process applies for a state method built using the Factory
:
from miros import ActiveObject
from miros import signals, Event, return_status
from miros import Factory
# create the specific behavior we want in our state chart
def trans_to_fc1(chart, e):
return chart.trans(fc1)
chart = Factory('factory_class_recipe_example')
fc = chart.create(state='fc'). \
catch(signal=signals.BB, handler=trans_to_fc). \
catch(signal=signals.INIT_SIGNAL, handler=trans_to_fc1). \
to_method()
chart.to_code(fc) # =>
# @spy_on
# def fc(chart, e):
# status = return_status.UNHANDLED
# if(e.signal == signals.ENTRY_SIGNAL):
# status = return_status.HANDLED
# elif(e.signal == signals.INIT_SIGNAL):
# status = trans_to_fc1(chart, e)
# elif(e.signal == signals.BB):
# status = trans_to_fc(chart, e)
# elif(e.signal == signals.EXIT_SIGNAL):
# status = return_status.HANDLED
# else:
# status, chart.temp.fun = return_status.SUPER, chart.top
# return status
To see how to unwind an auto-generated statechart read unwinding a state method
Describing your Work#
Drawing a StateChart#
The Harel formalism was consumed by the UML standard.
The UML standards were not properly curated and became overly-complicated and full of contradictions. As a result, they are largely disregarded by the software community, so most of the open source drawing tool projects have been abandoned.
A lot of the commercial drawing tools have tried to keep up with the overly complicated UML standards, so you end up fighting with the tools when you just want to draw a simple picture. The point of the picture is to be expressive enough to explain something to someone else.
So in many ways UML has become a kind of anti-brand but it has its good parts. Skip the class diagrams and use the sequence diagram and the statecharts.
A statechart drawing tool only needs to provide the following features:
zoom in and out of a diagram.
draw the basic Harel statemachine building blocks.
draw arrows and the other useful parts of UML.
mark up the diagram with code
be simple to change a design
Pencil and paper are great for drawing your designs. It is good to work on them over and over again without the impediment of the computer interface getting in your way.
Once you think you have it figured out you can transfer the picture into something digital using a free tool called umlet.
There is also an online version of the tool, which is called umletino.
It is easy to use and it has a lot of youtube training videos. It doesn’t provide the zooming features asked for by the original Harel paper (1987), but this could be implemented using HTML/SVG if you have a lot of spare time.
If you want to drop an ASCII art picture into your code (which you will see in the examples) you can use the drawit vim plugin, or something like it for your text editor. If you don’t know how to do it yet, look up vertical editing, this is required if you are going to sketch in meaningful pictures.
Drawing a Sequence Diagram#
To create effective, yet inexpensive documentation, you can first obtain a trace of your system, then use it to generate a sequence diagram, with sequence.
Without a lot of effort, you can configure your text editor to write these pictures for you. When I select this in my editor:
[2017-11-06 08:34:28.268873] [75c8c] e->start_at() top->arming
[2017-11-06 08:34:26.312241] [75c8c] e->BC() arming->armed
[2017-11-06 08:34:26.312241] [75c8c] e->BC() armed->armed
[2017-11-06 08:34:26.312241] [75c8c] e->BC() armed->armed
Then press <ctrl-T>, it becomes this:
[ Chart: 75c8c ] (?)
top arming armed
+-tart_at()->| |
| (?) | |
| +---BC()---->|
| | (?) |
| | +
| | \ (?)
| | BC()
| | /
| | <
Then I would manually replace the question marks with numbers, so that I could explained each event by referencing its number. Since my diagram is in ASCII, I could place it in my code comments.
sequence also works with interleaving trace outputs that would come from two different interacting active objects:
Suppose you got this from your terminal while testing two different statecharts:
[2017-11-06 08:34:28.268873] [75c8c] e->start_at() top->arming
[2017-11-06 08:34:28.268873] [95a8c] e->start_at() top->arming
[2017-11-06 08:34:26.312241] [75c8c] e->BC() arming->armed
[2017-11-06 08:34:26.312241] [95a8c] e->OTHER() arming->armed
[2017-11-06 08:34:26.312241] [75c8c] e->BC() armed->armed
[2017-11-06 08:34:26.312241] [75c8c] e->BC() armed->armed
[2017-11-06 08:34:26.312241] [95a8c] e->BC() armed->armed
[2017-11-06 08:34:26.312241] [95a8c] e->BC() armed->armed
By running it through sequence we would see:
[ Chart: 75c8c ] (?)
top arming armed
+-tart_at()->| |
| (?) | |
| +---BC()---->|
| | (?) |
| | +
| | \ (?)
| | BC()
| | /
| | <
[ Chart: 95a8c ] (?)
top arming armed
+-tart_at()->| |
| (?) | |
| +--OTHER()-->|
| | (?) |
| | +
| | \ (?)
| | BC()
| | /
| | <
Now I’ll write some fake documentation to make a point, notice how I update the numbers in the diagram:
[ Chart: Unit 1 ]
top arming armed
+start_at()->| |
| (1) | |
| +---BC()---->|
| | (3) |
| | +
| | \ (5)
| | BC()
| | /
| | <
[ Chart: Unit 2 ]
top arming armed
+start_at()->| |
| (2) | |
| +--OTHER()-->|
| | (4) |
| | +
| | \ (6)
| | BC()
| | /
| | <
You can gang two tazors together to act as one tazor. The first arming event in your tazor network will also arm all of the other tazors, consider the diagram above to see this interaction.
Tazor labeled ‘Unit 1’ turns on in the arming state.
Tazor labeled ‘Unit 2’ turns on in the arming state.
Unit 1 begins a battery charge (BC) which will send a broadcast message to all other tazors in the network.
Unit 2 detects another tazor is beginning a battery charge, so it too begins its battery charge (OTHER)
…. and so on
If I changed the above design, it would be simple to adjust these diagrams and their description. Sequence diagrams are great for explaining small things, but they do break the DRY principle. You are effectively replicating your data by having these descriptions in your documentation. The source image is your state chart diagram. Give it a lot of attention, since it is actually your specification. The sequence diagrams are little throw away things, that can be used to assist in telling a very specific story about how your system behaves.
I’m probably not following the UML standard and I don’t care. The utility of the sequence diagram picture is in its simplicity.
I know that you can represent loops and object destructor’s using these diagrams, but why bother? It is easier to write a loop in the code than it is in a picture, so why not copy and paste the code onto the sequence diagram if you need to explain a loop?
If you would like to create sequence diagrams that are UML compliant, the umlet program supports these features.
Testing#
Using a Trace As a Test Target#
If you would like to sketch out the high level behavior of your statechart using a trace output as the target for a regression test, you would:
Run your program and print your trace to the output.
Copy this trace as the target behavior of your test.
Run this trace target and the future output of the statechart trace through the stripped context manager to remove the date timestamp information.
Compare your target with the results.
This should become clear with an example.
Consider a statechart that outputted the following at the point when you were working on it.
[2017-11-05 21:31:56.098526] [tazor] e->start_at() top->arming
[2017-11-05 21:31:56.200047] [tazor] e->BATTERY_CHARGE() arming->armed
[2017-11-05 21:31:56.300974] [tazor] e->BATTERY_CHARGE() armed->armed
[2017-11-05 21:31:56.401682] [tazor] e->BATTERY_CHARGE() armed->armed
You might have gotten this output with the following code:
print(tazor.trace())
It does a decent job describing what we want, but it has timestamps. With the stripped context manager we can turn the above into something that would look like this:
[tazor] e->start_at() top->arming
[tazor] e->BATTERY_CHARGE() arming->armed
[tazor] e->BATTERY_CHARGE() armed->armed
[tazor] e->BATTERY_CHARGE() armed->armed
That is something that shouldn’t change over time, it looks like something we could use as a test specification. The only problem is that when we run the code in the future and generate a new trace we get a trace with a pre-pended date timestamp. We can get around this issue like this:
1from miros import stripped
2
3target = \
4'''[2017-11-05 21:31:56.098526] [tazor] e->start_at() top->arming
5 [2017-11-05 21:31:56.200047] [tazor] e->BATTERY_CHARGE() arming->armed
6 [2017-11-05 21:31:56.300974] [tazor] e->BATTERY_CHARGE() armed->armed
7 [2017-11-05 21:31:56.401682] [tazor] e->BATTERY_CHARGE() armed->armed
8'''
9with stripped(target) as stripped_target, \
10 stripped(tazor.trace()) as stripped_trace_result:
11
12 for target, result in zip(stripped_target, stripped_trace_result):
13 assert(target == result)
On line 1 we import the stripped context manager.
On lines 3-7 we define the target as just being the trace that we copied when we were first designing our statechart.
On line 9, we map this target to the stripped_target
which contains the
same string with the date timestamps removed.
On line 10, we use the same stripped context manager to strip our tazor active
object’s trace output of its date timestamp information and place the result
into the stripped_trace_result
variable.
Lines 12-13 are for testing each line of our output against our target.
If our design changes, it is easy to update the test, we just copy the new trace of of new design into the target string and everything should be peachy.
Using a Spy as a Test Target#
A trace does not tell the full story about what your system is doing. For instance it is blind to hooks, deferred events and many other things that might happen in the dynamics of your active object. If you need to look at the exact behavior of your system, you can:
Run your program and print your spy to the output.
Copy the spy as your target behavior.
Compare the target with the results.
Here is an example:
1# pp(tazor.spy())
2# import pdb.set_trace()
3assert(tazor.spy() ==
4 ['START',
5 'SEARCH_FOR_SUPER_SIGNAL:arming',
6 'SEARCH_FOR_SUPER_SIGNAL:tazor_operating',
7 'ENTRY_SIGNAL:tazor_operating',
8 'ENTRY_SIGNAL:arming',
9 'INIT_SIGNAL:arming',
10 '<- Queued:(0) Deferred:(0)',
11 'BATTERY_CHARGE:arming',
12 'SEARCH_FOR_SUPER_SIGNAL:armed',
13 'ENTRY_SIGNAL:armed',
14 'POST_DEFERRED:CAPACITOR_CHARGE',
15 'INIT_SIGNAL:armed',
16 '<- Queued:(0) Deferred:(1)'])
On line 1 we have a commented pretty print command ready to go for when we need to rebuild our test specification. When the test fails in the future, which it will because this is a tightly coupled test, we will uncomment lines 1-2 then re-run the test.
This will drop us into a debugging session just after our next spy output has been printed to the screen. At this point we would carefully determine if it actually describes the new behavior we are looking for. If it wasn’t, we would fix the issue, otherwise we over-write lines 4-16 with this new specification.
We would re-comment lines 1-2 and re-run our tests.