1
0
mirror of https://github.com/JoelBender/bacpypes synced 2025-09-28 22:15:23 +08:00
bacpypes/tests/state_machine.py
2018-08-15 16:29:30 -04:00

1325 lines
43 KiB
Python
Executable File

#!/usr/bin/python
"""
Testing State Machine
---------------------
"""
try:
# Python 3
from queue import Queue
except ImportError:
# Python 2
from Queue import Queue
from bacpypes.debugging import bacpypes_debugging, ModuleLogger
from bacpypes.comm import Client, Server
from bacpypes.task import OneShotTask
from .time_machine import current_time
# some debugging
_debug = 0
_log = ModuleLogger(globals())
class Transition:
"""
Transition
~~~~~~~~~~
Instances of this class are transitions betweeen states of a state
machine.
"""
def __init__(self, next_state):
self.next_state = next_state
class SendTransition(Transition):
def __init__(self, pdu, next_state):
Transition.__init__(self, next_state)
self.pdu = pdu
class ReceiveTransition(Transition):
def __init__(self, criteria, next_state):
Transition.__init__(self, next_state)
self.criteria = criteria
class EventTransition(Transition):
def __init__(self, event_id, next_state):
Transition.__init__(self, next_state)
self.event_id = event_id
class TimeoutTransition(Transition):
def __init__(self, timeout, next_state):
Transition.__init__(self, next_state)
self.timeout = timeout
class CallTransition(Transition):
def __init__(self, fnargs, next_state):
Transition.__init__(self, next_state)
# a tuple of (fn, *args, *kwargs)
self.fnargs = fnargs
#
# match_pdu
#
@bacpypes_debugging
def match_pdu(pdu, pdu_type=None, **pdu_attrs):
if _debug: match_pdu._debug("match_pdu %r %r %r", pdu, pdu_type, pdu_attrs)
# check the type
if pdu_type and not isinstance(pdu, pdu_type):
if _debug: match_pdu._debug(" - failed match, wrong type")
return False
# check for matching attribute values
for attr_name, attr_value in pdu_attrs.items():
if not hasattr(pdu, attr_name):
if _debug: match_pdu._debug(" - failed match, missing attr: %r", attr_name)
return False
if getattr(pdu, attr_name) != attr_value:
if _debug: StateMachine._debug(" - failed match, attr value: %r, %r", attr_name, attr_value)
return False
if _debug: match_pdu._debug(" - successful match")
return True
#
# TimeoutTask
#
@bacpypes_debugging
class TimeoutTask(OneShotTask):
def __init__(self, fn, *args, **kwargs):
if _debug: TimeoutTask._debug("__init__ %r %r %r", fn, args, kwargs)
OneShotTask.__init__(self)
# save the function and args
self.fn = fn
self.args = args
self.kwargs = kwargs
def process_task(self):
if _debug: TimeoutTask._debug("process_task %r", self.fn)
self.fn(*self.args, **self.kwargs)
def __repr__(self):
return "<%s of %r at %s>" % (
self.__class__.__name__,
self.fn,
hex(id(self)),
)
#
# State
#
@bacpypes_debugging
class State(object):
"""
State
~~~~~
Instances of this class, or a derived class, are the states of a state
machine.
"""
def __init__(self, state_machine, doc_string=""):
"""Create a new state, bound to a specific state machine. This is
typically called by the state machine.
"""
if _debug:
State._debug(
"__init__ %r doc_string=%r", state_machine, doc_string
)
self.state_machine = state_machine
self.doc_string = doc_string
self.is_success_state = False
self.is_fail_state = False
# empty lists of send and receive transitions
self.send_transitions = []
self.receive_transitions = []
# empty lists of event transitions
self.set_event_transitions = []
self.clear_event_transitions = []
self.wait_event_transitions = []
# timeout transition
self.timeout_transition = None
# call transition
self.call_transition = None
def reset(self):
"""Override this method in a derived class if the state maintains
counters or other information. Called when the associated state
machine is reset.
"""
if _debug: State._debug("reset")
def doc(self, doc_string):
"""Change the documentation string (label) for the state. The state
is returned for method chaining.
"""
if _debug: State._debug("doc %r", doc_string)
# save the doc string
self.doc_string = doc_string
# chainable
return self
def success(self, doc_string=None):
"""Mark a state as a successful final state. The state is returned
for method chaining.
:param doc_string: an optional label for the state
"""
if _debug: State._debug("success %r", doc_string)
# error checking
if self.is_success_state:
raise RuntimeError("already a success state")
if self.is_fail_state:
raise RuntimeError("already a fail state")
# this is now a success state
self.is_success_state = True
# save the new doc string
if doc_string is not None:
self.doc_string = doc_string
elif not self.doc_string:
self.doc_string = "success"
# chainable
return self
def fail(self, doc_string=None):
"""Mark a state as a failure final state. The state is returned
for method chaining.
:param doc_string: an optional label for the state
"""
if _debug: State._debug("fail %r", doc_string)
# error checking
if self.is_success_state:
raise RuntimeError("already a success state")
if self.is_fail_state:
raise RuntimeError("already a fail state")
# this is now a fail state
self.is_fail_state = True
# save the new doc string
if doc_string is not None:
self.doc_string = doc_string
elif not self.doc_string:
self.doc_string = "fail"
# chainable
return self
def enter_state(self):
"""Called when the state machine is entering the state."""
if _debug: State._debug("enter_state(%s)", self.doc_string)
# if there is a timeout, schedule it
if self.timeout_transition:
if _debug: State._debug(" - waiting: %r", self.timeout_transition.timeout)
# schedule the timeout
self.state_machine.state_timeout_task.install_task(delta=self.timeout_transition.timeout)
else:
if _debug: State._debug(" - no timeout")
def exit_state(self):
"""Called when the state machine is exiting the state."""
if _debug: State._debug("exit_state(%s)", self.doc_string)
# if there was a timeout, suspend it
if self.timeout_transition:
if _debug: State._debug(" - canceling timeout")
self.state_machine.state_timeout_task.suspend_task()
def send(self, pdu, next_state=None):
"""Create a SendTransition from this state to another, possibly new,
state. The next state is returned for method chaining.
:param pdu: PDU to send
:param next_state: state to transition to after sending
"""
if _debug: State._debug("send(%s) %r next_state=%r", self.doc_string, pdu, next_state)
# maybe build a new state
if not next_state:
next_state = self.state_machine.new_state()
if _debug: State._debug(" - new next_state: %r", next_state)
elif next_state not in self.state_machine.states:
raise ValueError("off the rails")
# add this to the list of transitions
self.send_transitions.append(SendTransition(pdu, next_state))
# return the next state
return next_state
def before_send(self, pdu):
"""Called before each PDU about to be sent."""
self.state_machine.before_send(pdu)
def after_send(self, pdu):
"""Called after each PDU sent."""
self.state_machine.after_send(pdu)
def receive(self, pdu_type, **pdu_attrs):
"""Create a ReceiveTransition from this state to another, possibly new,
state. The next state is returned for method chaining.
:param criteria: PDU to match
:param next_state: destination state after a successful match
Simulate the function as if it was defined in Python3.5+ like this:
def receive(self, *pdu_type, next_state=None, **pdu_attrs)
"""
if _debug: State._debug("receive(%s) %r %r", self.doc_string, pdu_type, pdu_attrs)
# extract the next_state keyword argument
if 'next_state' in pdu_attrs:
next_state = pdu_attrs['next_state']
if _debug: State._debug(" - next_state: %r", next_state)
del pdu_attrs['next_state']
else:
next_state = None
# maybe build a new state
if not next_state:
next_state = self.state_machine.new_state()
if _debug: State._debug(" - new next_state: %r", next_state)
elif next_state not in self.state_machine.states:
raise ValueError("off the rails")
# create a bundle of the match criteria
criteria = (pdu_type, pdu_attrs)
if _debug: State._debug(" - criteria: %r", criteria)
# add this to the list of transitions
self.receive_transitions.append(ReceiveTransition(criteria, next_state))
# return the next state
return next_state
def before_receive(self, pdu):
"""Called with each PDU received before matching."""
self.state_machine.before_receive(pdu)
def after_receive(self, pdu):
"""Called with PDU received after match."""
self.state_machine.after_receive(pdu)
def ignore(self, pdu_type, **pdu_attrs):
"""Create a ReceiveTransition from this state to itself, if match
is successful the effect is to ignore the PDU.
:param criteria: PDU to match
"""
if _debug: State._debug("ignore(%s) %r %r", self.doc_string, pdu_type, pdu_attrs)
# create a bundle of the match criteria
criteria = (pdu_type, pdu_attrs)
if _debug: State._debug(" - criteria: %r", criteria)
# add this to the list of transitions
self.receive_transitions.append(ReceiveTransition(criteria, self))
# return this state, no new state is created
return self
def unexpected_receive(self, pdu):
"""Called with PDU that did not match. Unless this is trapped by the
state, the default behaviour is to fail."""
if _debug: State._debug("unexpected_receive(%s) %r", self.doc_string, pdu)
# pass along to the state machine
self.state_machine.unexpected_receive(pdu)
def set_event(self, event_id):
"""Create an EventTransition for this state that sets an event. The
current state is returned for method chaining.
:param event_id: event identifier
"""
if _debug: State._debug("set_event(%s) %r", self.doc_string, event_id)
# add this to the list of transitions
self.set_event_transitions.append(EventTransition(event_id, None))
# return the next state
return self
def event_set(self, event_id):
"""Called with the event that was set."""
pass
def clear_event(self, event_id):
"""Create an EventTransition for this state that clears an event. The
current state is returned for method chaining.
:param event_id: event identifier
"""
if _debug: State._debug("clear_event(%s) %r", self.doc_string, event_id)
# add this to the list of transitions
self.clear_event_transitions.append(EventTransition(event_id, None))
# return the next state
return next_state
def wait_event(self, event_id, next_state=None):
"""Create an EventTransition from this state to another, possibly new,
state. The next state is returned for method chaining.
:param pdu: PDU to send
:param next_state: state to transition to after sending
"""
if _debug: State._debug("wait_event(%s) %r next_state=%r", self.doc_string, event_id, next_state)
# maybe build a new state
if not next_state:
next_state = self.state_machine.new_state()
if _debug: State._debug(" - new next_state: %r", next_state)
elif next_state not in self.state_machine.states:
raise ValueError("off the rails")
# add this to the list of transitions
self.wait_event_transitions.append(EventTransition(event_id, next_state))
# return the next state
return next_state
def timeout(self, delay, next_state=None):
"""Create a TimeoutTransition from this state to another, possibly new,
state. There can only be one timeout transition per state. The next
state is returned for method chaining.
:param delay: the amount of time to wait for a matching PDU
:param next_state: destination state after timeout
"""
if _debug: State._debug("timeout(%s) %r next_state=%r", self.doc_string, delay, next_state)
# check to see if a timeout has already been specified
if self.timeout_transition:
raise RuntimeError("state already has a timeout")
# maybe build a new state
if not next_state:
next_state = self.state_machine.new_state()
if _debug: State._debug(" - new next_state: %r", next_state)
elif next_state not in self.state_machine.states:
raise ValueError("off the rails")
# set the transition
self.timeout_transition = TimeoutTransition(delay, next_state)
# return the next state
return next_state
def call(self, fn, *args, **kwargs):
"""Create a CallTransition from this state to another, possibly new,
state. The next state is returned for method chaining.
:param criteria: PDU to match
:param next_state: destination state after a successful match
Simulate the function as if it was defined in Python3.5+ like this:
def receive(self, fn, *args, next_state=None, **kwargs)
"""
if _debug: State._debug("call(%s) %r %r %r", self.doc_string, fn, args, kwargs)
# only one call per state
if self.call_transition:
raise RuntimeError("only one 'call' per state")
# extract the next_state keyword argument
if 'next_state' in kwargs:
next_state = kwargs['next_state']
if _debug: State._debug(" - next_state: %r", next_state)
del kwargs['next_state']
else:
next_state = None
# maybe build a new state
if not next_state:
next_state = self.state_machine.new_state()
if _debug: State._debug(" - new next_state: %r", next_state)
elif next_state not in self.state_machine.states:
raise ValueError("off the rails")
# create a bundle of the match criteria
fnargs = (fn, args, kwargs)
if _debug: State._debug(" - fnargs: %r", fnargs)
# add this to the list of transitions
self.call_transition = CallTransition(fnargs, next_state)
# return the next state
return next_state
def __repr__(self):
return "<%s(%s) at %s>" % (
self.__class__.__name__,
self.doc_string,
hex(id(self)),
)
@bacpypes_debugging
class StateMachine(object):
"""
StateMachine
~~~~~~~~~~~~
A state machine consisting of states. Every state machine has a start
state where the state machine begins when it is started. It also has
an *unexpected receive* fail state where the state machine goes if
there is an unexpected (unmatched) PDU received.
"""
def __init__(
self,
timeout=None,
start_state=None,
unexpected_receive_state=None,
machine_group=None,
state_subclass=State,
name='',
):
if _debug: StateMachine._debug("__init__(%s)", name)
# save the name for debugging
self.name = name
# no states to starting out, not running
self.states = []
self.running = False
# flags for remembering success or fail
self.is_success_state = None
self.is_fail_state = None
# might be part of a group
self.machine_group = machine_group
# reset to initial condition
self.reset()
# save the state subclass for new states
if not issubclass(state_subclass, State):
raise TypeError("%r is not derived from State" % (state_subclass,))
self.state_subclass = state_subclass
# create the start state
if start_state:
if start_state.state_machine:
raise RuntimeError("start state already bound to a machine")
self.states.append(start_state)
start_state.state_machine = self
else:
start_state = self.new_state("start")
self.start_state = start_state
# create the unexpected receive state
if unexpected_receive_state:
if unexpected_receive_state.state_machine:
raise RuntimeError("unexpected receive state already bound to a machine")
self.states.append(unexpected_receive_state)
unexpected_receive_state.state_machine = self
else:
unexpected_receive_state = self.new_state("unexpected receive").fail()
self.unexpected_receive_state = unexpected_receive_state
# received messages get queued during state transitions
self.state_transitioning = 0
self.transition_queue = Queue()
# create a state timeout task, to be installed as necessary
self.state_timeout_task = TimeoutTask(self.state_timeout)
# create a state machine timeout task
self.timeout = timeout
if timeout:
self.timeout_state = self.new_state("state machine timeout").fail()
self.timeout_task = TimeoutTask(self.state_machine_timeout)
else:
self.timeout_state = None
self.timeout_task = None
def new_state(self, doc="", state_subclass=None):
if _debug: StateMachine._debug("new_state(%s) %r %r", self.name, doc, state_subclass)
# check for proper subclass
if state_subclass and not issubclass(state_subclass, State):
raise TypeError("%r is not derived from State" % (state_subclass,))
# make the state object from the class that was provided or default
state = (state_subclass or self.state_subclass)(self, doc)
if _debug: StateMachine._debug(" - state: %r", state)
# save a reference to make sure we don't go off the rails
self.states.append(state)
# return the new state
return state
def reset(self):
if _debug: StateMachine._debug("reset(%s)", self.name)
# make sure we're not running
if self.running:
raise RuntimeError("state machine running")
# flags for remembering success or fail
self.is_success_state = None
self.is_fail_state = None
# no current state, empty transaction log
self.current_state = None
self.transaction_log = []
# we are not starting up
self._startup_flag = False
# give all the states a chance to reset
for state in self.states:
state.reset()
def run(self):
if _debug: StateMachine._debug("run(%s)", self.name)
if self.running:
raise RuntimeError("state machine running")
if self.current_state:
raise RuntimeError("not running but has a current state")
# if there is a timeout task, schedule the fail
if self.timeout_task:
if _debug: StateMachine._debug(" - schedule runtime limit")
self.timeout_task.install_task(delta=self.timeout)
# we are starting up
self._startup_flag = True
# go to the start state
self.goto_state(self.start_state)
# startup complete
self._startup_flag = False
# if it is part of a group, let the group know
if self.machine_group:
self.machine_group.started(self)
# if it stopped already, let the group know
if not self.running:
self.machine_group.stopped(self)
def halt(self):
"""Called when the state machine should no longer be running."""
if _debug: StateMachine._debug("halt(%s)", self.name)
# make sure we're running
if not self.running:
raise RuntimeError("state machine not running")
# cancel the timeout task
if self.timeout_task:
if _debug: StateMachine._debug(" - cancel runtime limit")
self.timeout_task.suspend_task()
# no longer running
self.running = False
def success(self):
"""Called when the state machine has successfully completed."""
if _debug: StateMachine._debug("success(%s)", self.name)
# flags for remembering success or fail
self.is_success_state = True
def fail(self):
"""Called when the state machine has failed."""
if _debug: StateMachine._debug("fail(%s)", self.name)
# flags for remembering success or fail
self.is_fail_state = True
def goto_state(self, state):
if _debug: StateMachine._debug("goto_state(%s) %r", self.name, state)
# where do you think you're going?
if state not in self.states:
raise RuntimeError("off the rails")
# transitioning
self.state_transitioning += 1
# exit the old state
if self.current_state:
self.current_state.exit_state()
elif state is self.start_state:
# starting up
self.running = True
else:
raise RuntimeError("start at the start state")
# here we are
current_state = self.current_state = state
# let the state do something
current_state.enter_state()
if _debug: StateMachine._debug(" - state entered")
# events are managed by a state machine group
if self.machine_group:
# setting events
for transition in current_state.set_event_transitions:
if _debug: StateMachine._debug(" - setting event: %r", transition.event_id)
self.machine_group.set_event(transition.event_id)
# clearing events
for transition in current_state.clear_event_transitions:
if _debug: StateMachine._debug(" - clearing event: %r", transition.event_id)
self.machine_group.clear_event(transition.event_id)
# check for success state
if current_state.is_success_state:
if _debug: StateMachine._debug(" - success state")
self.state_transitioning -= 1
self.halt()
self.success()
# if it is part of a group, let the group know
if self.machine_group and not self._startup_flag:
self.machine_group.stopped(self)
return
# check for fail state
if current_state.is_fail_state:
if _debug: StateMachine._debug(" - fail state")
self.state_transitioning -= 1
self.halt()
self.fail()
# if it is part of a group, let the group know
if self.machine_group and not self._startup_flag:
self.machine_group.stopped(self)
return
# assume we can stay
next_state = None
# events are managed by a state machine group
if self.machine_group:
# check to see if there are events that are already set
for transition in current_state.wait_event_transitions:
if _debug: StateMachine._debug(" - waiting event: %r", transition.event_id)
# check for a transition
if transition.event_id in self.machine_group.events:
next_state = transition.next_state
if _debug: StateMachine._debug(" - next_state: %r", next_state)
if next_state is not current_state:
break
else:
if _debug: StateMachine._debug(" - no events already set")
else:
if _debug: StateMachine._debug(" - not part of a group")
# call things that need to be called
if current_state.call_transition:
if _debug: StateMachine._debug(" - calling: %r", current_state.call_transition)
# pull apart the pieces and call it
fn, args, kwargs = current_state.call_transition.fnargs
try:
fn( *args, **kwargs)
if _debug: StateMachine._debug(" - called, no exception")
# check for a transition
next_state = current_state.call_transition.next_state
if _debug: StateMachine._debug(" - next_state: %r", next_state)
except AssertionError as err:
if _debug: StateMachine._debug(" - called, exception: %r", err)
self.state_transitioning -= 1
self.halt()
self.fail()
# if it is part of a group, let the group know
if self.machine_group and not self._startup_flag:
self.machine_group.stopped(self)
return
else:
if _debug: StateMachine._debug(" - no calls")
# send everything that needs to be sent
if not next_state:
for transition in current_state.send_transitions:
if _debug: StateMachine._debug(" - sending: %r", transition)
current_state.before_send(transition.pdu)
self.send(transition.pdu)
current_state.after_send(transition.pdu)
# check for a transition
next_state = transition.next_state
if _debug: StateMachine._debug(" - next_state: %r", next_state)
if next_state is not current_state:
break
if not next_state:
if _debug: StateMachine._debug(" - nowhere to go")
elif next_state is self.current_state:
if _debug: StateMachine._debug(" - going nowhere")
else:
if _debug: StateMachine._debug(" - going")
self.goto_state(next_state)
# no longer transitioning
self.state_transitioning -= 1
# could be recursive call
if not self.state_transitioning:
while self.running and not self.transition_queue.empty():
pdu = self.transition_queue.get()
if _debug: StateMachine._debug(" - pdu: %r", pdu)
# try again
self.receive(pdu)
def before_send(self, pdu):
"""Called before each PDU about to be sent."""
# add a reference to the pdu in the transaction log
self.transaction_log.append(("<<<", pdu),)
def send(self, pdu):
raise NotImplementedError("send not implemented")
def after_send(self, pdu):
"""Called after each PDU sent."""
pass
def before_receive(self, pdu):
"""Called with each PDU received before matching."""
# add a reference to the pdu in the transaction log
self.transaction_log.append((">>>", pdu),)
def receive(self, pdu):
if _debug: StateMachine._debug("receive(%s) %r", self.name, pdu)
# check to see if haven't started yet or we are transitioning
if (not self.current_state) or self.state_transitioning:
if _debug: StateMachine._debug(" - queue for later")
self.transition_queue.put(pdu)
return
# if this is not running it already completed
if not self.running:
if _debug: StateMachine._debug(" - already completed")
return
# reference the current state
current_state = self.current_state
if _debug: StateMachine._debug(" - current_state: %r", current_state)
# let the state know this was received
current_state.before_receive(pdu)
match_found = False
# look for a matching receive transition
for transition in current_state.receive_transitions:
if self.match_pdu(pdu, transition.criteria):
if _debug: StateMachine._debug(" - match found")
match_found = True
# let the state know this was matched
current_state.after_receive(pdu)
# check for a transition
next_state = transition.next_state
if _debug: StateMachine._debug(" - next_state: %r", next_state)
# a match was found, but by transitioning back to the
# current state, the pdu will not be "unexpectedly received"
# and there could be a subsequent match
if next_state is not current_state:
break
else:
if _debug: StateMachine._debug(" - no matches")
if not match_found:
if _debug: StateMachine._debug(" - unexpected")
current_state.unexpected_receive(pdu)
elif next_state is not current_state:
if _debug: StateMachine._debug(" - going")
self.goto_state(next_state)
def after_receive(self, pdu):
"""Called with PDU received after match."""
pass
def unexpected_receive(self, pdu):
"""Called with PDU that did not match. Unless this is trapped by the
state, the default behaviour is to fail."""
if _debug:
StateMachine._debug("unexpected_receive(%s) %r", self.name, pdu)
StateMachine._debug(" - current_state: %r", self.current_state)
# go to the unexpected receive state (failing)
self.goto_state(self.unexpected_receive_state)
def event_set(self, event_id):
"""Called by the state machine group when an event is set, the state
machine checks to see if it's waiting for the event and makes the
state transition if there is a match."""
if _debug: StateMachine._debug("event_set(%s) %r", self.name, event_id)
if not self.running:
if _debug: StateMachine._debug(" - not running")
return
# check to see if we are transitioning
if self.state_transitioning:
if _debug: StateMachine._debug(" - transitioning")
return
if not self.current_state:
raise RuntimeError("no current state")
current_state = self.current_state
match_found = False
# look for a matching event transition
for transition in current_state.wait_event_transitions:
if transition.event_id == event_id:
if _debug: StateMachine._debug(" - match found")
match_found = True
# let the state know this event was set
current_state.event_set(event_id)
# check for a transition
next_state = transition.next_state
if _debug: StateMachine._debug(" - next_state: %r", next_state)
if next_state is not current_state:
break
else:
if _debug: StateMachine._debug(" - going nowhere")
if match_found and next_state is not current_state:
if _debug: StateMachine._debug(" - going")
self.goto_state(next_state)
def state_timeout(self):
if _debug: StateMachine._debug("state_timeout(%s)", self.name)
if not self.running:
raise RuntimeError("state machine not running")
if not self.current_state.timeout_transition:
raise RuntimeError("state timeout, but no timeout transition")
# go to the state specified
self.goto_state(self.current_state.timeout_transition.next_state)
def state_machine_timeout(self):
if _debug: StateMachine._debug("state_machine_timeout(%s)", self.name)
if not self.running:
raise RuntimeError("state machine not running")
# go to the state specified
self.goto_state(self.timeout_state)
def match_pdu(self, pdu, criteria):
if _debug: StateMachine._debug("match_pdu(%s) %r %r", self.name, pdu, criteria)
# separate the pdu_type and attributes to match
pdu_type, pdu_attrs = criteria
# pass along to the global function
return match_pdu(pdu, pdu_type, **pdu_attrs)
def __repr__(self):
if not self.current_state:
state_text = "not started"
elif self.is_success_state:
state_text = "success"
elif self.is_fail_state:
state_text = "fail"
elif not self.running:
state_text = "idle"
else:
state_text = "in"
if self.current_state:
state_text += " " + repr(self.current_state)
return "<%s(%s) %s at %s>" % (
self.__class__.__name__,
self.name,
state_text,
hex(id(self)),
)
@bacpypes_debugging
class StateMachineGroup(object):
"""
StateMachineGroup
~~~~~~~~~~~~~~~~~
A state machine group is a collection of state machines that are all
started and stopped together. There are methods available to derived
classes that are called when all of the machines in the group have
completed, either all successfully or at least one has failed.
.. note:: When creating a group of state machines, add the ones that
are expecting to receive one or more PDU's first before the ones
that send PDU's. They will be started first, and be ready for the
PDU that might be sent.
"""
def __init__(self):
"""Create a state machine group."""
if _debug: StateMachineGroup._debug("__init__")
# empty list of machines
self.state_machines = []
# flag for starting up
self._startup_flag = False
# flag for at least one machine running
self.is_running = False
# flags for remembering success or fail
self.is_success_state = None
self.is_fail_state = None
# set of events that are set
self.events = set()
def append(self, state_machine):
"""Add a state machine to the end of the list of state machines."""
if _debug: StateMachineGroup._debug("append %r", state_machine)
# check the state machine
if not isinstance(state_machine, StateMachine):
raise TypeError("not a state machine")
if state_machine.machine_group:
raise RuntimeError("state machine already a part of a group")
# tell the state machine it is a member of this group
state_machine.machine_group = self
# add it to the list
self.state_machines.append(state_machine)
def remove(self, state_machine):
"""Remove a state machine from the list of state machines."""
if _debug: StateMachineGroup._debug("remove %r", state_machine)
# check the state machine
if not isinstance(state_machine, StateMachine):
raise TypeError("not a state machine")
if state_machine.machine_group is not self:
raise RuntimeError("state machine not a member of this group")
# tell the state machine it is no longer a member of this group
state_machine.machine_group = None
# pass along to the list
self.state_machines.remove(state_machine)
def reset(self):
"""Resets all the machines in the group."""
if _debug: StateMachineGroup._debug("reset")
# pass along to each machine
for state_machine in self.state_machines:
if _debug: StateMachineGroup._debug(" - resetting: %r", state_machine)
state_machine.reset()
# flags for remembering success or fail
self.is_success_state = False
self.is_fail_state = False
# events that are set
self.events = set()
def set_event(self, event_id):
"""Save an event as 'set' and pass it to the state machines to see
if they are in a state that is waiting for the event."""
if _debug: StateMachineGroup._debug("set_event %r", event_id)
self.events.add(event_id)
if _debug: StateMachineGroup._debug(" - event set")
# pass along to each machine
for state_machine in self.state_machines:
if _debug: StateMachineGroup._debug(" - state_machine: %r", state_machine)
state_machine.event_set(event_id)
def clear_event(self, event_id):
"""Remove an event from the set of elements that are 'set'."""
if _debug: StateMachineGroup._debug("clear_event %r", event_id)
if event_id in self.events:
self.events.remove(event_id)
if _debug: StateMachineGroup._debug(" - event cleared")
else:
if _debug: StateMachineGroup._debug(" - noop")
def run(self):
"""Runs all the machines in the group."""
if _debug: StateMachineGroup._debug("run")
# turn on the startup flag
self._startup_flag = True
self.is_running = True
# pass along to each machine
for state_machine in self.state_machines:
if _debug: StateMachineGroup._debug(" - starting: %r", state_machine)
state_machine.run()
# turn off the startup flag
self._startup_flag = False
if _debug: StateMachineGroup._debug(" - all started")
# check for success/fail, all of the machines may already be done
all_success, some_failed = self.check_for_success()
if all_success:
self.success()
elif some_failed:
self.fail()
else:
if _debug: StateMachineGroup._debug(" - some still running")
def started(self, state_machine):
"""Called by a state machine in the group when it has completed its
transition into its starting state."""
if _debug: StateMachineGroup._debug("started %r", state_machine)
def stopped(self, state_machine):
"""Called by a state machine after it has halted and its success()
or fail() method has been called."""
if _debug: StateMachineGroup._debug("stopped %r", state_machine)
# if we are starting up try again later
if self._startup_flag:
if _debug: StateMachineGroup._debug(" - still starting up")
return
all_success, some_failed = self.check_for_success()
if all_success:
self.success()
elif some_failed:
self.fail()
else:
if _debug: StateMachineGroup._debug(" - some still running")
def check_for_success(self):
"""Called after all of the machines have started, and each time a
machine has stopped, to see if the entire group should be considered
a success or fail."""
if _debug: StateMachineGroup._debug("check_for_success")
# accumulators
all_success = True
some_failed = False
# check each machine
for state_machine in self.state_machines:
if state_machine.running:
if _debug: StateMachineGroup._debug(" - running: %r", state_machine)
all_success = some_failed = None
break
# if there is no current state it hasn't started
if not state_machine.current_state:
if _debug: StateMachineGroup._debug(" - not started: %r", state_machine)
all_success = some_failed = None
continue
all_success = all_success and state_machine.current_state.is_success_state
some_failed = some_failed or state_machine.current_state.is_fail_state
if _debug:
StateMachineGroup._debug(" - all_success: %r", all_success)
StateMachineGroup._debug(" - some_failed: %r", some_failed)
# return the results of the check
return (all_success, some_failed)
def halt(self):
"""Halts all of the running machines in the group."""
if _debug: StateMachineGroup._debug("halt")
# pass along to each machine
for state_machine in self.state_machines:
if state_machine.running:
state_machine.halt()
def success(self):
"""Called when all of the machines in the group have halted and they
are all in a 'success' final state."""
if _debug: StateMachineGroup._debug("success")
self.is_running = False
self.is_success_state = True
def fail(self):
"""Called when all of the machines in the group have halted and at
at least one of them is in a 'fail' final state."""
if _debug:
StateMachineGroup._debug("fail")
for state_machine in self.state_machines:
StateMachineGroup._debug(" - machine: %r", state_machine)
for direction, pdu in state_machine.transaction_log:
StateMachineGroup._debug(" %s %s", direction, str(pdu))
self.is_running = False
self.is_fail_state = True
@bacpypes_debugging
class ClientStateMachine(Client, StateMachine):
"""
ClientStateMachine
~~~~~~~~~~~~~~~~~~
An instance of this class sits at the top of a stack. PDU's that the
state machine sends are sent down the stack and PDU's coming up the
stack are fed as received PDU's.
"""
def __init__(self, name=''):
if _debug: ClientStateMachine._debug("__init__")
Client.__init__(self)
StateMachine.__init__(self, name=name)
def send(self, pdu):
if _debug: ClientStateMachine._debug("send(%s) %r", self.name, pdu)
self.request(pdu)
def confirmation(self, pdu):
if _debug: ClientStateMachine._debug("confirmation(%s) %r", self.name, pdu)
self.receive(pdu)
@bacpypes_debugging
class ServerStateMachine(Server, StateMachine):
"""
ServerStateMachine
~~~~~~~~~~~~~~~~~~
An instance of this class sits at the bottom of a stack. PDU's that the
state machine sends are sent up the stack and PDU's coming down the
stack are fed as received PDU's.
"""
def __init__(self, name=''):
if _debug: ServerStateMachine._debug("__init__")
Server.__init__(self)
StateMachine.__init__(self, name=name)
def send(self, pdu):
if _debug: ServerStateMachine._debug("send %r", pdu)
self.response(pdu)
def indication(self, pdu):
if _debug: ServerStateMachine._debug("indication %r", pdu)
self.receive(pdu)
#
# TrafficLog
#
class TrafficLog:
def __init__(self):
"""Initialize with no traffic."""
self.traffic = []
def __call__(self, *args):
"""Capture the current time and the arguments."""
self.traffic.append((current_time(),) + args)
def dump(self, handler_fn):
"""Dump the traffic, pass the correct handler like SomeClass._debug"""
for args in self.traffic:
arg_format = " %6.3f:"
for arg in args[1:]:
if hasattr(arg, 'debug_contents'):
arg_format += " %r"
else:
arg_format += " %s"
handler_fn(arg_format, *args)