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:

    1. an object of type/subclass of miros.ActiveObject, which has an event processor.

    2. 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:

_images/state_recipe_1.svg

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:

_images/state_recipe_2.svg

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:

_images/state_recipe_3.svg

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#

_images/state_recipe_4.svg

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.

_images/state_recipe_6.svg

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:

_images/state_recipe_7.svg

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.

_images/state_recipe_8.svg

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.

_images/state_recipe_9.svg

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.

_images/state_recipe_10.svg

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:

_images/state_recipe_11.svg

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.

_images/state_recipe_12.svg

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:

_images/state_recipe_13.svg

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).

_images/state_recipe_14.svg

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:

_images/state_recipe_14.svg

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).

_images/state_recipe_15.svg

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:

  1. setting the temp.fun attribute of the first argument to point at their parent state.

  2. 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.

_images/hook1.svg

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.

_images/catchandrelease1.svg

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:

_images/state_recipe_10.svg

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:

_images/state_recipe_10.svg

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#

_images/state_recipe_10.svg

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#

_images/state_recipe_10.svg

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:

_images/guard2.svg

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:

_images/named_tuple_payload.svg

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:

_images/start_at.svg

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.

Sharing Attributes between Threads (ActiveObjects)#

As of miros version v4.1.3 (fixed in v4.1.4), you can create thread-safe-attributes in your derived ActiveObject class by also inheriting the ThreadSafeAttributes.

To create one or more thread safe attribute, you add them to the list defined _attributes:

 1from miros import Event
 2from miros import spy_on
 3from miros import signals
 4from miros import ActiveObject
 5from miros import return_status
 6from miros import ThreadSafeAttributes
 7
 8class ThreadSafeAttributesInActiveObject(ThreadSafeAttributes, ActiveObject):
 9
10  _attributes = ['thread_safe_attr_1', 'thread_safe_attr_2']
11
12  def __init__(self, name):
13    super().__init__(name)
14
15 @spy_on
16 def c(chart, e):
17   status = return_status.UNHANDLED
18   if(e.signal == signals.ENTRY_SIGNAL):
19     chart.thread_safe_attr_1 = False
20     chart.thread_safe_attr_2 = False
21   elif(e.signal == signals.INIT_SIGNAL):
22     status = chart.trans(c1)
23   elif(e.signal == signals.B):
24     chart.thread_safe_attr_1 = True
25     status = chart.trans(c)
26   else:
27     chart.temp.fun = chart.top
28     status = return_status.SUPER
29   return status
30
31 @spy_on
32 def c1(chart, e):
33   status = return_status.UNHANDLED
34   if(e.signal == signals.ENTRY_SIGNAL):
35     chart.thread_safe_attr_1 = True
36     status = return_status.HANDLED
37   elif(e.signal == signals.A):
38     status = chart.trans(c2)
39   elif(e.signal == signals.EXIT_SIGNAL):
40     chart.thread_safe_attr_1 = False
41     status = return_status.HANDLED
42   else:
43     chart.temp.fun = c
44     status = return_status.SUPER
45   return status
46
47 @spy_on
48 def c2(chart, e):
49   status = return_status.UNHANDLED
50   if(e.signal == signals.ENTRY_SIGNAL):
51     chart.thread_safe_attr_2 = True
52     status = return_status.HANDLED
53   elif(e.signal == signals.A):
54     status = chart.trans(c1)
55   elif(e.signal == signals.EXIT_SIGNAL):
56     chart.thread_safe_attr_2 = False
57     status = return_status.HANDLED
58   else:
59     chart.temp.fun = c
60     status = return_status.SUPER
61   return status
62
63 if __name__ == '__main__':
64    ao = ThreadSafeAttributesInActiveObject("ao")
65    ao.start_at(c)
66    # Change the ActiveObject's attributes while it is starting its thread
67    # and starting its statemachine
68    ao.thread_safe_attr_1 = True
69    ao.thread_safe_attr_2 = False
70    ao.post_fifo(Event(signal=signals.A)
71    # Main thread can access attribute used by the ActiveObject's thread
72    print(ao.thread_safe_attr_2)

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__ method

  • hides 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 avoided

  • it 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:

_images/factory_in_class_simple.svg

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:

_images/factory_in_class.svg

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:

_images/factory_in_class_compact.svg

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:

_images/factory_in_class_compact_2.svg

Then to draw a federation:

_images/federation_drawing.svg

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.

Sharing Attributes between Threads (Factories)#

As of miros version v4.1.3 (fixed in v4.1.4), you can create thread-safe-attributes in your derived Factory class by inheriting from ThreadSafeAttributes.

To create one or more thread safe attribute, you add them to the list defined _attributes within the class body before the __init__ method is defined. See below:

from miros import Event
from miros import signals
from miros import Factory
from miros import return_status
from miros import ThreadSafeAttributes

# By inheriting from ThreadSafeAttributes, you can define as many thread safe
# attributes you need by assigning their names as strings to the
# `_attributes` list.
class Example2(Factory, ThreadSafeAttributes):

  _attributes = ['thread_safe_attr_1', 'thread_safe_attr_2']

def __init__(self, name, live_trace=None, live_spy=None):

  super().__init__(name=name)
  self.thread_safe_attr_1 = True  # .. you can access this from main or other
                                  # threads in the same way

  # ... define states, link them to their signals
  # ... nesting code, etc.

  # statechart thread is created and started when `start_at` is called
  self.start_at(self.c)  #

# ... state methods defined here

if __name__ == "__main__":

  statechart = Example2('example2')     # thread is started and is running
                                        # because `start_at` called within
                                        # the `__init__` method.

  print(statechart.thread_safe_attr_1)  # you can access the attribute which
                                        # is being used by the statechart's
                                        # thread, since it's wrapped
                                        # by a thread-safe datastructure

By inheriting from the ThreadSafeAttributes class, we get access to the _attributes feature. By assigning a list of strings to this _attributes class attribute, a set of thread safe attributes are created an initialized in the background before the Example2 __init__ method is called.

To see a full example look here.

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:

  1. If it subscribes to an event using the fifo strategy, the active fabric will post events to it using its post_fifo method.

  2. 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:

  1. A datetime stamp between square brackets

  2. The active object name, between square brackets

  3. The event that caused the transition, its signal number and its payload

  4. The starting state

  5. 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:

  1. All of the internal activity that is run by the event processor

  2. How your chart reacts to the events it has received

  3. Information about what happened between each of the rtc activities.

  4. If a signal was hooked by a state

  5. If and when a signal was deferred

  6. If and when a signal was recalled

  7. The number of events awaiting immediate processing after a specific rtc activity.

  8. 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:

  1. zoom in and out of a diagram.

  2. draw the basic Harel statemachine building blocks.

  3. draw arrows and the other useful parts of UML.

  4. mark up the diagram with code

  5. 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.

  1. Tazor labeled ‘Unit 1’ turns on in the arming state.

  2. Tazor labeled ‘Unit 2’ turns on in the arming state.

  3. Unit 1 begins a battery charge (BC) which will send a broadcast message to all other tazors in the network.

  4. 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:

  1. Run your program and print your trace to the output.

  2. Copy this trace as the target behavior of your test.

  3. Run this trace target and the future output of the statechart trace through the stripped context manager to remove the date timestamp information.

  4. 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:

  1. Run your program and print your spy to the output.

  2. Copy the spy as your target behavior.

  3. 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.