1
0
mirror of https://github.com/JoelBender/bacpypes synced 2025-09-28 22:15:23 +08:00

add a call() transition and tests, foreign device registration tests

This commit is contained in:
Joel Bender 2017-09-12 02:52:23 -04:00
parent 209d619994
commit 4c1ecffba6
7 changed files with 380 additions and 56 deletions

View File

@ -493,7 +493,7 @@ class BIPForeign(BIPSAP, Client, Server, OneShotTask, DebugContents):
# check for success
if pdu.bvlciResultCode == 0:
# schedule for a refresh
self.install_task(_time() + self.bbmdTimeToLive)
self.install_task(delta=self.bbmdTimeToLive)
return
@ -541,7 +541,7 @@ class BIPForeign(BIPSAP, Client, Server, OneShotTask, DebugContents):
self.bbmdTimeToLive = ttl
# install this task to run when it gets a chance
self.install_task(0)
self.install_task(delta=0)
def unregister(self):
"""Drop the registration with a BBMD."""

View File

@ -489,7 +489,7 @@ class BIPForeign(BIPSAP, Client, Server, OneShotTask, DebugContents):
# check for success
if pdu.bvlciResultCode == 0:
# schedule for a refresh
self.install_task(_time() + self.bbmdTimeToLive)
self.install_task(delta=self.bbmdTimeToLive)
return
@ -537,7 +537,7 @@ class BIPForeign(BIPSAP, Client, Server, OneShotTask, DebugContents):
self.bbmdTimeToLive = ttl
# install this task to run when it gets a chance
self.install_task(0)
self.install_task(delta=0)
def unregister(self):
"""Drop the registration with a BBMD."""

View File

@ -488,7 +488,7 @@ class BIPForeign(BIPSAP, Client, Server, OneShotTask, DebugContents):
# check for success
if pdu.bvlciResultCode == 0:
# schedule for a refresh
self.install_task(_time() + self.bbmdTimeToLive)
self.install_task(delta=self.bbmdTimeToLive)
return
@ -536,7 +536,7 @@ class BIPForeign(BIPSAP, Client, Server, OneShotTask, DebugContents):
self.bbmdTimeToLive = ttl
# install this task to run when it gets a chance
self.install_task(0)
self.install_task(delta=0)
def unregister(self):
"""Drop the registration with a BBMD."""

View File

@ -67,6 +67,15 @@ class TimeoutTransition(Transition):
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
#
@ -134,6 +143,9 @@ class State(object):
# 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
@ -307,7 +319,7 @@ class State(object):
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 %r", pdu)
if _debug: State._debug("unexpected_receive(%s) %r", self.doc_string, pdu)
# pass along to the state machine
self.state_machine.unexpected_receive(pdu)
@ -393,6 +405,48 @@ class State(object):
# 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__,
@ -421,8 +475,12 @@ class StateMachine(object):
unexpected_receive_state=None,
machine_group=None,
state_subclass=State,
name='',
):
if _debug: StateMachine._debug("__init__")
if _debug: StateMachine._debug("__init__(%s)", name)
# save the name for debugging
self.name = name
# no states to starting out, not running
self.states = []
@ -476,7 +534,7 @@ class StateMachine(object):
self.timeout_task = None
def new_state(self, doc="", state_subclass=None):
if _debug: StateMachine._debug("new_state %r %r", doc, state_subclass)
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):
@ -493,7 +551,7 @@ class StateMachine(object):
return state
def reset(self):
if _debug: StateMachine._debug("reset")
if _debug: StateMachine._debug("reset(%s)", self.name)
# make sure we're not running
if self.running:
@ -511,7 +569,7 @@ class StateMachine(object):
state.reset()
def run(self):
if _debug: StateMachine._debug("run")
if _debug: StateMachine._debug("run(%s)", self.name)
if self.running:
raise RuntimeError("state machine running")
@ -542,7 +600,7 @@ class StateMachine(object):
def halt(self):
"""Called when the state machine should no longer be running."""
if _debug: StateMachine._debug("halt")
if _debug: StateMachine._debug("halt(%s)", self.name)
# make sure we're running
if not self.running:
@ -558,14 +616,14 @@ class StateMachine(object):
def success(self):
"""Called when the state machine has successfully completed."""
if _debug: StateMachine._debug("success")
if _debug: StateMachine._debug("success(%s)", self.name)
def fail(self):
"""Called when the state machine has failed."""
if _debug: StateMachine._debug("fail")
if _debug: StateMachine._debug("fail(%s)", self.name)
def goto_state(self, state):
if _debug: StateMachine._debug("goto_state %r", 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:
@ -586,6 +644,10 @@ class StateMachine(object):
# 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
@ -626,10 +688,6 @@ class StateMachine(object):
return
# let the state do something
current_state.enter_state()
if _debug: StateMachine._debug(" - state entered")
# assume we can stay
next_state = None
@ -651,6 +709,21 @@ class StateMachine(object):
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
fn( *args, **kwargs)
if _debug: StateMachine._debug(" - called")
# check for a transition
next_state = current_state.call_transition.next_state
if _debug: StateMachine._debug(" - next_state: %r", next_state)
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:
@ -710,7 +783,7 @@ class StateMachine(object):
self.transaction_log.append((">>>", pdu),)
def receive(self, pdu):
if _debug: StateMachine._debug("receive %r", pdu)
if _debug: StateMachine._debug("receive(%s) %r", self.name, pdu)
if not self.running:
if _debug: StateMachine._debug(" - not running")
@ -726,6 +799,7 @@ class StateMachine(object):
if not self.current_state:
raise RuntimeError("no 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)
@ -766,7 +840,7 @@ class StateMachine(object):
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 %r", pdu)
if _debug: StateMachine._debug("unexpected_receive(%s) %r", self.name, pdu)
# go to the unexpected receive state (failing)
self.goto_state(self.unexpected_receive_state)
@ -775,7 +849,7 @@ class StateMachine(object):
"""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 %r", event_id)
if _debug: StateMachine._debug("event_set(%s) %r", self.name, event_id)
if not self.running:
if _debug: StateMachine._debug(" - not running")
@ -815,7 +889,7 @@ class StateMachine(object):
self.goto_state(next_state)
def state_timeout(self):
if _debug: StateMachine._debug("state_timeout")
if _debug: StateMachine._debug("state_timeout(%s)", self.name)
if not self.running:
raise RuntimeError("state machine not running")
@ -826,7 +900,7 @@ class StateMachine(object):
self.goto_state(self.current_state.timeout_transition.next_state)
def state_machine_timeout(self):
if _debug: StateMachine._debug("state_machine_timeout")
if _debug: StateMachine._debug("state_machine_timeout(%s)", self.name)
if not self.running:
raise RuntimeError("state machine not running")
@ -835,7 +909,7 @@ class StateMachine(object):
self.goto_state(self.timeout_state)
def match_pdu(self, pdu, criteria):
if _debug: StateMachine._debug("match_pdu %r %r", 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
@ -1062,7 +1136,12 @@ class StateMachineGroup(object):
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")
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
@ -1120,3 +1199,4 @@ class ServerStateMachine(Server, StateMachine):
def indication(self, pdu):
if _debug: ServerStateMachine._debug("indication %r", pdu)
self.receive(pdu)

View File

@ -112,7 +112,8 @@ class SnifferNode(_repr, ClientStateMachine):
if _debug: SnifferNode._debug("__init__ %r %r", address, vlan)
ClientStateMachine.__init__(self)
# save the address
# save the name and address
self.name = address
self.address = Address(address)
# create a promiscuous node, added to the network
@ -123,6 +124,31 @@ class SnifferNode(_repr, ClientStateMachine):
bind(self, self.node)
#
# CodecNode
#
@bacpypes_debugging
class CodecNode(_repr, ClientStateMachine):
def __init__(self, address, vlan):
if _debug: CodecNode._debug("__init__ %r %r", address, vlan)
ClientStateMachine.__init__(self)
# save the name and address
self.name = address
self.address = Address(address)
# BACnet/IP interpreter
self.annexj = AnnexJCodec()
# fake multiplexer has a VLAN node in it
self.mux = FauxMultiplexer(self.address, vlan)
# bind the stack together
bind(self, self.annexj, self.mux)
#
# SimpleNode
#
@ -134,7 +160,8 @@ class SimpleNode(_repr, ClientStateMachine):
if _debug: SimpleNode._debug("__init__ %r %r", address, vlan)
ClientStateMachine.__init__(self)
# save the address
# save the name and address
self.name = address
self.address = Address(address)
# BACnet/IP interpreter
@ -159,7 +186,8 @@ class ForeignNode(_repr, ClientStateMachine):
if _debug: ForeignNode._debug("__init__ %r %r", address, vlan)
ClientStateMachine.__init__(self)
# save the address
# save the name and address
self.name = address
self.address = Address(address)
# BACnet/IP interpreter
@ -183,17 +211,24 @@ class BBMDNode(_repr, ClientStateMachine):
if _debug: BBMDNode._debug("__init__ %r %r", address, vlan)
ClientStateMachine.__init__(self)
# save the address
# save the name and address
self.name = address
self.address = Address(address)
# BACnet/IP interpreter
self.bip = BIPBBMD(self.address)
self.annexj = AnnexJCodec()
# build an address, full mask
bdt_address = "%s/32:%d" % self.address.addrTuple
if _debug: BBMDNode._debug(" - bdt_address: %r", bdt_address)
# add itself as the first entry in the BDT
self.bip.add_peer(Address(bdt_address))
# fake multiplexer has a VLAN node in it
self.mux = FauxMultiplexer(self.address, vlan)
# bind the stack together
bind(self, self.bip, self.annexj, self.mux)

View File

@ -12,11 +12,12 @@ from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob
from bacpypes.pdu import Address, PDU, LocalBroadcast
from bacpypes.vlan import IPNetwork, IPRouter
from bacpypes.bvll import ReadForeignDeviceTable, ReadForeignDeviceTableAck
from ..state_machine import match_pdu, StateMachineGroup
from ..state_machine import StateMachineGroup
from ..time_machine import reset_time_machine, run_time_machine
from .helpers import SnifferNode, SimpleNode, ForeignNode, BBMDNode
from .helpers import SnifferNode, CodecNode, SimpleNode, ForeignNode, BBMDNode
# some debugging
_debug = 0
@ -49,26 +50,14 @@ class TNetwork(StateMachineGroup):
self.home_vlan = IPNetwork()
self.router.add_network(Address("192.168.5.1/24"), self.home_vlan)
# home sniffer node
self.home_sniffer = SnifferNode("192.168.5.254/24", self.home_vlan)
self.append(self.home_sniffer)
# make a remote LAN
self.remote_vlan = IPNetwork()
self.router.add_network(Address("192.168.6.1/24"), self.remote_vlan)
# remote sniffer node
self.remote_sniffer = SnifferNode("192.168.6.254/24", self.remote_vlan)
self.append(self.remote_sniffer)
# the foreign device
self.fd = ForeignNode("192.168.6.2/24", self.remote_vlan)
self.append(self.fd)
# intermediate test node
self.tnode = SimpleNode("192.168.5.2/24", self.home_vlan)
self.append(self.tnode)
# bbmd
self.bbmd = BBMDNode("192.168.5.3/24", self.home_vlan)
self.append(self.bbmd)
@ -81,12 +70,7 @@ class TNetwork(StateMachineGroup):
# run it for some time
run_time_machine(time_limit)
if _debug:
TNetwork._debug(" - time machine finished")
for state_machine in self.state_machines:
TNetwork._debug(" - machine: %r", state_machine)
for direction, pdu in state_machine.transaction_log:
TNetwork._debug(" %s %s", direction, str(pdu))
if _debug: TNetwork._debug(" - time machine finished")
# check for success
all_success, some_failed = super(TNetwork, self).check_for_success()
@ -94,22 +78,227 @@ class TNetwork(StateMachineGroup):
@bacpypes_debugging
class TestSimple(unittest.TestCase):
class TestForeign(unittest.TestCase):
def test_idle(self):
"""Test an idle network, nothing happens is success."""
if _debug: TestSimple._debug("test_idle")
if _debug: TestForeign._debug("test_idle")
# create a network
tnet = TNetwork()
# all start states are successful
tnet.home_sniffer.start_state.success()
tnet.remote_sniffer.start_state.success()
tnet.fd.start_state.success()
tnet.tnode.start_state.success()
tnet.bbmd.start_state.success()
# run the group
tnet.run()
def test_registration(self):
"""Test foreign device registration."""
if _debug: TestForeign._debug("test_registration")
# create a network
tnet = TNetwork()
# add an addition codec node to the home vlan
cnode = CodecNode("192.168.5.2/24", tnet.home_vlan)
tnet.append(cnode)
# home sniffer node
home_sniffer = SnifferNode("192.168.5.254/24", tnet.home_vlan)
tnet.append(home_sniffer)
# remote sniffer node
remote_sniffer = SnifferNode("192.168.6.254/24", tnet.remote_vlan)
tnet.append(remote_sniffer)
# tell the B/IP layer of the foreign device to register
tnet.fd.start_state \
.call(tnet.fd.bip.register, tnet.bbmd.address, 30) \
.success()
# sniffer pieces
registration_request = xxtob('81.05.0006' # bvlci
'001e' # time-to-live
)
registration_ack = xxtob('81.00.0006.0000') # simple ack
# remote sniffer sees registration
remote_sniffer.start_state \
.receive(PDU, pduData=registration_request).doc("--1-1") \
.receive(PDU, pduData=registration_ack).doc("--1-2") \
.set_event('fd-registered').doc("--1-3") \
.success()
# the bbmd is idle
tnet.bbmd.start_state.success()
# read the FDT
cnode.start_state \
.wait_event('fd-registered').doc("--1-4") \
.send(ReadForeignDeviceTable(destination=tnet.bbmd.address)).doc("--1-5") \
.receive(ReadForeignDeviceTableAck).doc("--1-6") \
.success()
# the tnode reads the registration table
read_fdt_request = xxtob('81.06.0004') # bvlci
read_fdt_ack = xxtob('81.07.000e' # read-ack
'c0.a8.06.02.ba.c0 001e 0023' # address, ttl, remaining
)
# home sniffer sees registration
home_sniffer.start_state \
.receive(PDU, pduData=registration_request).doc("--1-7") \
.receive(PDU, pduData=registration_ack).doc("--1-8") \
.receive(PDU, pduData=read_fdt_request).doc("--1-9") \
.receive(PDU, pduData=read_fdt_ack).doc("--1-A") \
.success()
# run the group
tnet.run()
def test_refresh_registration(self):
"""Test refreshing foreign device registration."""
if _debug: TestForeign._debug("test_refresh_registration")
# create a network
tnet = TNetwork()
# tell the B/IP layer of the foreign device to register
tnet.fd.start_state \
.call(tnet.fd.bip.register, tnet.bbmd.address, 10) \
.success()
# the bbmd is idle
tnet.bbmd.start_state.success()
# remote sniffer node
remote_sniffer = SnifferNode("192.168.6.254/24", tnet.remote_vlan)
tnet.append(remote_sniffer)
# sniffer pieces
registration_request = xxtob('81.05.0006' # bvlci
'000a' # time-to-live
)
registration_ack = xxtob('81.00.0006.0000') # simple ack
# remote sniffer sees registration
remote_sniffer.start_state \
.receive(PDU, pduData=registration_request).doc("--2-1") \
.receive(PDU, pduData=registration_ack).doc("--2-2") \
.receive(PDU, pduData=registration_request).doc("--2-3") \
.receive(PDU, pduData=registration_ack).doc("--2-4") \
.success()
# run the group
tnet.run()
def test_unicast(self):
"""Test a unicast message from the foreign device to the bbmd."""
if _debug: TestForeign._debug("test_unicast")
# create a network
tnet = TNetwork()
# make a PDU from node 1 to node 2
pdu_data = xxtob('dead.beef')
pdu = PDU(pdu_data, source=tnet.fd.address, destination=tnet.bbmd.address)
if _debug: TestForeign._debug(" - pdu: %r", pdu)
# register, wait for ack, send some beef
tnet.fd.start_state \
.call(tnet.fd.bip.register, tnet.bbmd.address, 60).doc("--3-1") \
.wait_event('fd-registered').doc("--3-2") \
.send(pdu).doc("--3-3") \
.success()
# the bbmd is happy when it gets the pdu
tnet.bbmd.start_state \
.receive(PDU, pduSource=tnet.fd.address, pduData=pdu_data) \
.success()
# remote sniffer node
remote_sniffer = SnifferNode("192.168.6.254/24", tnet.remote_vlan)
tnet.append(remote_sniffer)
# sniffer pieces
registration_request = xxtob('81.05.0006' # bvlci
'003c' # time-to-live (60)
)
registration_ack = xxtob('81.00.0006.0000') # simple ack
unicast_pdu = xxtob('81.0a.0008' # original unicast bvlci
'dead.beef' # PDU being unicast
)
# remote sniffer sees registration
remote_sniffer.start_state \
.receive(PDU, pduData=registration_request).doc("--3-4") \
.receive(PDU, pduData=registration_ack).doc("--3-5") \
.set_event('fd-registered').doc("--3-6") \
.receive(PDU, pduData=unicast_pdu).doc("--3-7") \
.success()
# run the group
tnet.run()
def test_broadcast(self):
"""Test a broadcast message from the foreign device to the bbmd."""
if _debug: TestForeign._debug("+test_broadcast")
# create a network
tnet = TNetwork()
# make a broadcast pdu
pdu_data = xxtob('dead.beef')
pdu = PDU(pdu_data, destination=LocalBroadcast())
if _debug: TestForeign._debug(" - pdu: %r", pdu)
# register, wait for ack, send some beef
tnet.fd.start_state \
.call(tnet.fd.bip.register, tnet.bbmd.address, 60).doc("--4-1") \
.wait_event('4-registered').doc("--4-2") \
.send(pdu).doc("--4-3") \
.success()
# the bbmd is happy when it gets the pdu
tnet.bbmd.start_state \
.receive(PDU, pduSource=tnet.fd.address, pduData=pdu_data) \
.success()
# home sniffer node
home_node = SimpleNode("192.168.5.254/24", tnet.home_vlan)
tnet.append(home_node)
# home node happy when getting the pdu, broadcast by the bbmd
home_node.start_state.doc("--4-4") \
.receive(PDU, pduSource=tnet.fd.address, pduData=pdu_data).doc("--4-5") \
.success()
# remote sniffer node
remote_sniffer = SnifferNode("192.168.6.254/24", tnet.remote_vlan)
tnet.append(remote_sniffer)
# sniffer pieces
registration_request = xxtob('81.05.0006' # bvlci
'003c' # time-to-live (60)
)
registration_ack = xxtob('81.00.0006.0000') # simple ack
distribute_pdu = xxtob('81.09.0008' # bvlci
'deadbeef' # PDU to broadcast
)
# remote sniffer sees registration
remote_sniffer.start_state \
.receive(PDU, pduData=registration_request).doc("--4-6") \
.call(TestForeign._debug, "::--4-5").doc("--4-7") \
.receive(PDU, pduData=registration_ack).doc("--4-8") \
.set_event('4-registered') \
.call(TestForeign._debug, "::--4-7").doc("--4-9") \
.receive(PDU, pduData=distribute_pdu).doc("--4-10") \
.success()
# run the group
tnet.run(4.0)
if _debug: TestForeign._debug("-test_broadcast")

View File

@ -262,6 +262,26 @@ class TestStateMachine(unittest.TestCase):
assert tsm.transaction_log[0][1] is bad_pdu
if _debug: TestStateMachine._debug(" - passed")
def test_state_machine_call(self):
if _debug: TestStateMachine._debug("test_state_machine_call")
# simple hook
self._called = False
# create a trapped state machine
tsm = TrappedStateMachine()
# make a send transition from start to success, run the machine
tsm.start_state.call(setattr, self, '_called', True).success()
tsm.run()
# check for success
assert not tsm.running
assert tsm.current_state.is_success_state
# check for the call
assert self._called
def test_state_machine_loop_01(self):
if _debug: TestStateMachine._debug("test_state_machine_loop_01")