mirror of
https://github.com/JoelBender/bacpypes
synced 2025-09-28 22:15:23 +08:00
1527 lines
58 KiB
Python
Executable File
1527 lines
58 KiB
Python
Executable File
#!/usr/bin/python
|
|
|
|
"""
|
|
Application Layer
|
|
"""
|
|
|
|
from time import time as _time
|
|
|
|
from .debugging import ModuleLogger, DebugContents, bacpypes_debugging
|
|
|
|
from .comm import Client, ServiceAccessPoint, ApplicationServiceElement
|
|
from .task import OneShotTask
|
|
|
|
from .pdu import Address
|
|
from .apdu import AbortPDU, AbortReason, ComplexAckPDU, \
|
|
ConfirmedRequestPDU, Error, ErrorPDU, RejectPDU, SegmentAckPDU, \
|
|
SimpleAckPDU, UnconfirmedRequestPDU, apdu_types, \
|
|
unconfirmed_request_types, confirmed_request_types, complex_ack_types, \
|
|
error_types
|
|
from .errors import RejectException, AbortException
|
|
|
|
# some debugging
|
|
_debug = 0
|
|
_log = ModuleLogger(globals())
|
|
|
|
#
|
|
# SSM - Segmentation State Machine
|
|
#
|
|
|
|
# transaction states
|
|
IDLE = 0
|
|
SEGMENTED_REQUEST = 1
|
|
AWAIT_CONFIRMATION = 2
|
|
AWAIT_RESPONSE = 3
|
|
SEGMENTED_RESPONSE = 4
|
|
SEGMENTED_CONFIRMATION = 5
|
|
COMPLETED = 6
|
|
ABORTED = 7
|
|
|
|
class SSM(OneShotTask, DebugContents):
|
|
|
|
transactionLabels = ['IDLE'
|
|
, 'SEGMENTED_REQUEST', 'AWAIT_CONFIRMATION', 'AWAIT_RESPONSE'
|
|
, 'SEGMENTED_RESPONSE', 'SEGMENTED_CONFIRMATION', 'COMPLETED', 'ABORTED'
|
|
]
|
|
|
|
_debug_contents = ('ssmSAP', 'localDevice', 'remoteDevice', 'invokeID'
|
|
, 'state', 'segmentAPDU', 'segmentSize', 'segmentCount', 'maxSegmentsAccepted'
|
|
, 'retryCount', 'segmentRetryCount', 'sentAllSegments', 'lastSequenceNumber'
|
|
, 'initialSequenceNumber', 'actualWindowSize', 'proposedWindowSize'
|
|
)
|
|
|
|
def __init__(self, sap, remoteDevice):
|
|
"""Common parts for client and server segmentation."""
|
|
if _debug: SSM._debug("__init__ %r %r", sap, remoteDevice)
|
|
OneShotTask.__init__(self)
|
|
|
|
self.ssmSAP = sap # service access point
|
|
self.remoteDevice = remoteDevice # remote device information, a DeviceInfo instance
|
|
self.invokeID = None # invoke ID
|
|
|
|
self.state = IDLE # initial state
|
|
self.segmentAPDU = None # refers to request or response
|
|
self.segmentSize = None # how big the pieces are
|
|
self.segmentCount = None
|
|
|
|
self.retryCount = None
|
|
self.segmentRetryCount = None
|
|
self.sentAllSegments = None
|
|
self.lastSequenceNumber = None
|
|
self.initialSequenceNumber = None
|
|
self.actualWindowSize = None
|
|
self.proposedWindowSize = None
|
|
|
|
# the maximum number of segments starts out being what's in the SAP
|
|
# which is the defaults or values from the local device.
|
|
self.maxSegmentsAccepted = self.ssmSAP.maxSegmentsAccepted
|
|
|
|
def start_timer(self, msecs):
|
|
if _debug: SSM._debug("start_timer %r", msecs)
|
|
|
|
# if this is active, pull it
|
|
if self.isScheduled:
|
|
if _debug: SSM._debug(" - is scheduled")
|
|
self.suspend_task()
|
|
|
|
# now install this
|
|
self.install_task(delta=msecs / 1000.0)
|
|
|
|
def stop_timer(self):
|
|
if _debug: SSM._debug("stop_timer")
|
|
|
|
# if this is active, pull it
|
|
if self.isScheduled:
|
|
if _debug: SSM._debug(" - is scheduled")
|
|
self.suspend_task()
|
|
|
|
def restart_timer(self, msecs):
|
|
if _debug: SSM._debug("restart_timer %r", msecs)
|
|
|
|
# if this is active, pull it
|
|
if self.isScheduled:
|
|
if _debug: SSM._debug(" - is scheduled")
|
|
self.suspend_task()
|
|
|
|
# now install this
|
|
self.install_task(delta=msecs / 1000.0)
|
|
|
|
def set_state(self, newState, timer=0):
|
|
"""This function is called when the derived class wants to change state."""
|
|
if _debug: SSM._debug("set_state %r (%s) timer=%r", newState, SSM.transactionLabels[newState], timer)
|
|
|
|
# make sure we have a correct transition
|
|
if (self.state == COMPLETED) or (self.state == ABORTED):
|
|
e = RuntimeError("invalid state transition from %s to %s" % (SSM.transactionLabels[self.state], SSM.transactionLabels[newState]))
|
|
SSM._exception(e)
|
|
raise e
|
|
|
|
# stop any current timer
|
|
self.stop_timer()
|
|
|
|
# make the change
|
|
self.state = newState
|
|
|
|
# if another timer should be started, start it
|
|
if timer:
|
|
self.start_timer(timer)
|
|
|
|
def set_segmentation_context(self, apdu):
|
|
"""This function is called to set the segmentation context."""
|
|
if _debug: SSM._debug("set_segmentation_context %s", repr(apdu))
|
|
|
|
# set the context
|
|
self.segmentAPDU = apdu
|
|
|
|
def get_segment(self, indx):
|
|
"""This function returns an APDU coorisponding to a particular
|
|
segment of a confirmed request or complex ack. The segmentAPDU
|
|
is the context."""
|
|
if _debug: SSM._debug("get_segment %r", indx)
|
|
|
|
# check for no context
|
|
if not self.segmentAPDU:
|
|
raise RuntimeError("no segmentation context established")
|
|
|
|
# check for invalid segment number
|
|
if indx >= self.segmentCount:
|
|
raise RuntimeError("invalid segment number %r, APDU has %r segments" % (indx, self.segmentCount))
|
|
|
|
if self.segmentAPDU.apduType == ConfirmedRequestPDU.pduType:
|
|
if _debug: SSM._debug(" - confirmed request context")
|
|
|
|
segAPDU = ConfirmedRequestPDU(self.segmentAPDU.apduService)
|
|
|
|
segAPDU.apduMaxSegs = self.maxSegmentsAccepted
|
|
segAPDU.apduMaxResp = self.ssmSAP.maxApduLengthAccepted
|
|
segAPDU.apduInvokeID = self.invokeID;
|
|
|
|
# segmented response accepted?
|
|
segAPDU.apduSA = self.ssmSAP.segmentationSupported in ('segmentedReceive', 'segmentedBoth')
|
|
if _debug: SSM._debug(" - segmented response accepted: %r", segAPDU.apduSA)
|
|
|
|
elif self.segmentAPDU.apduType == ComplexAckPDU.pduType:
|
|
if _debug: SSM._debug(" - complex ack context")
|
|
|
|
segAPDU = ComplexAckPDU(self.segmentAPDU.apduService, self.segmentAPDU.apduInvokeID)
|
|
else:
|
|
raise RuntimeError("invalid APDU type for segmentation context")
|
|
|
|
# maintain the the user data reference
|
|
segAPDU.pduUserData = self.segmentAPDU.pduUserData
|
|
|
|
# make sure the destination is set
|
|
segAPDU.pduDestination = self.remoteDevice.address
|
|
|
|
# segmented message?
|
|
if (self.segmentCount != 1):
|
|
segAPDU.apduSeg = True
|
|
segAPDU.apduMor = (indx < (self.segmentCount - 1)) # more follows
|
|
segAPDU.apduSeq = indx % 256 # sequence number
|
|
segAPDU.apduWin = self.proposedWindowSize # window size
|
|
else:
|
|
segAPDU.apduSeg = False
|
|
segAPDU.apduMor = False
|
|
|
|
# add the content
|
|
offset = indx * self.segmentSize
|
|
segAPDU.put_data( self.segmentAPDU.pduData[offset:offset+self.segmentSize] )
|
|
|
|
# success
|
|
return segAPDU
|
|
|
|
def append_segment(self, apdu):
|
|
"""This function appends the apdu content to the end of the current
|
|
APDU being built. The segmentAPDU is the context."""
|
|
if _debug: SSM._debug("append_segment %r", apdu)
|
|
|
|
# check for no context
|
|
if not self.segmentAPDU:
|
|
raise RuntimeError("no segmentation context established")
|
|
|
|
# append the data
|
|
self.segmentAPDU.put_data(apdu.pduData)
|
|
|
|
def in_window(self, seqA, seqB):
|
|
if _debug: SSM._debug("in_window %r %r", seqA, seqB)
|
|
|
|
rslt = ((seqA - seqB + 256) % 256) < self.actualWindowSize
|
|
if _debug: SSM._debug(" - rslt: %r", rslt)
|
|
|
|
return rslt
|
|
|
|
def FillWindow(self, seqNum):
|
|
"""This function sends all of the packets necessary to fill
|
|
out the segmentation window."""
|
|
if _debug: SSM._debug("FillWindow %r", seqNum)
|
|
|
|
for ix in range(self.actualWindowSize):
|
|
apdu = self.get_segment(seqNum + ix)
|
|
|
|
# send the message
|
|
self.ssmSAP.request(apdu)
|
|
|
|
# check for no more follows
|
|
if not apdu.apduMor:
|
|
self.sentAllSegments = True
|
|
break
|
|
|
|
bacpypes_debugging(SSM)
|
|
|
|
#
|
|
# ClientSSM - Client Segmentation State Machine
|
|
#
|
|
|
|
class ClientSSM(SSM):
|
|
|
|
def __init__(self, sap, remoteDevice):
|
|
if _debug: ClientSSM._debug("__init__ %s %r", sap, remoteDevice)
|
|
SSM.__init__(self, sap, remoteDevice)
|
|
|
|
# initialize the retry count
|
|
self.retryCount = 0
|
|
|
|
def set_state(self, newState, timer=0):
|
|
"""This function is called when the client wants to change state."""
|
|
if _debug: ClientSSM._debug("set_state %r (%s) timer=%r", newState, SSM.transactionLabels[newState], timer)
|
|
|
|
# do the regular state change
|
|
SSM.set_state(self, newState, timer)
|
|
|
|
# when completed or aborted, remove tracking
|
|
if (newState == COMPLETED) or (newState == ABORTED):
|
|
if _debug: ClientSSM._debug(" - remove from active transactions")
|
|
self.ssmSAP.clientTransactions.remove(self)
|
|
|
|
if _debug: ClientSSM._debug(" - release device information")
|
|
self.ssmSAP.deviceInfoCache.release_device_info(self.remoteDevice)
|
|
|
|
def request(self, apdu):
|
|
"""This function is called by client transaction functions when it wants
|
|
to send a message to the device."""
|
|
if _debug: ClientSSM._debug("request %r", apdu)
|
|
|
|
# make sure it has a good source and destination
|
|
apdu.pduSource = None
|
|
apdu.pduDestination = self.remoteDevice.address
|
|
|
|
# send it via the device
|
|
self.ssmSAP.request(apdu)
|
|
|
|
def indication(self, apdu):
|
|
"""This function is called after the device has bound a new transaction
|
|
and wants to start the process rolling."""
|
|
if _debug: ClientSSM._debug("indication %r", apdu)
|
|
|
|
# make sure we're getting confirmed requests
|
|
if (apdu.apduType != ConfirmedRequestPDU.pduType):
|
|
raise RuntimeError("invalid APDU (1)")
|
|
|
|
# save the request and set the segmentation context
|
|
self.set_segmentation_context(apdu)
|
|
|
|
# the segment size is the minimum of the maximum size I can transmit,
|
|
# the maximum conveyable by the internetwork to the remote device, and
|
|
# the maximum APDU size accepted by the remote device.
|
|
self.segmentSize = min(
|
|
self.ssmSAP.maxApduLengthAccepted,
|
|
self.remoteDevice.maxNpduLength,
|
|
self.remoteDevice.maxApduLengthAccepted,
|
|
)
|
|
if _debug: ClientSSM._debug(" - segment size: %r", self.segmentSize)
|
|
|
|
# the maximum number of segments acceptable in the reply
|
|
if apdu.apduMaxSegs is not None:
|
|
# this request overrides the default
|
|
self.maxSegmentsAccepted = apdu.apduMaxSegs
|
|
|
|
# save the invoke ID
|
|
self.invokeID = apdu.apduInvokeID
|
|
if _debug: ClientSSM._debug(" - invoke ID: %r", self.invokeID)
|
|
|
|
# compute the segment count ### minus the header?
|
|
if not apdu.pduData:
|
|
# always at least one segment
|
|
self.segmentCount = 1
|
|
else:
|
|
# split into chunks, maybe need one more
|
|
self.segmentCount, more = divmod(len(apdu.pduData), self.segmentSize)
|
|
if more:
|
|
self.segmentCount += 1
|
|
if _debug: ClientSSM._debug(" - segment count: %r", self.segmentCount)
|
|
|
|
# make sure we support segmented transmit if we need to
|
|
if self.segmentCount > 1:
|
|
if self.ssmSAP.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'):
|
|
if _debug: ClientSSM._debug(" - local device can't send segmented requests")
|
|
abort = self.abort(AbortReason.segmentationNotSupported)
|
|
self.response(abort)
|
|
return
|
|
if self.remoteDevice.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'):
|
|
if _debug: ClientSSM._debug(" - remote device can't receive segmented requests")
|
|
abort = self.abort(AbortReason.segmentationNotSupported)
|
|
self.response(abort)
|
|
return
|
|
|
|
### check for APDUTooLong?
|
|
|
|
# send out the first segment (or the whole thing)
|
|
if self.segmentCount == 1:
|
|
# SendConfirmedUnsegmented
|
|
self.sentAllSegments = True
|
|
self.retryCount = 0
|
|
self.set_state(AWAIT_CONFIRMATION, self.ssmSAP.retryTimeout)
|
|
else:
|
|
# SendConfirmedSegmented
|
|
self.sentAllSegments = False
|
|
self.retryCount = 0
|
|
self.segmentRetryCount = 0
|
|
self.initialSequenceNumber = 0
|
|
self.proposedWindowSize = self.ssmSAP.maxSegmentsAccepted
|
|
self.actualWindowSize = 1
|
|
self.set_state(SEGMENTED_REQUEST, self.ssmSAP.segmentTimeout)
|
|
|
|
# deliver to the device
|
|
self.request(self.get_segment(0))
|
|
|
|
def response(self, apdu):
|
|
"""This function is called by client transaction functions when they want
|
|
to send a message to the application."""
|
|
if _debug: ClientSSM._debug("response %r", apdu)
|
|
|
|
# make sure it has a good source and destination
|
|
apdu.pduSource = self.remoteDevice.address
|
|
apdu.pduDestination = None
|
|
|
|
# send it to the application
|
|
self.ssmSAP.sap_response(apdu)
|
|
|
|
def confirmation(self, apdu):
|
|
"""This function is called by the device for all upstream messages related
|
|
to the transaction."""
|
|
if _debug: ClientSSM._debug("confirmation %r", apdu)
|
|
|
|
if self.state == SEGMENTED_REQUEST:
|
|
self.segmented_request(apdu)
|
|
elif self.state == AWAIT_CONFIRMATION:
|
|
self.await_confirmation(apdu)
|
|
elif self.state == SEGMENTED_CONFIRMATION:
|
|
self.segmented_confirmation(apdu)
|
|
else:
|
|
raise RuntimeError("invalid state")
|
|
|
|
def process_task(self):
|
|
"""This function is called when something has taken too long."""
|
|
if _debug: ClientSSM._debug("process_task")
|
|
|
|
if self.state == SEGMENTED_REQUEST:
|
|
self.segmented_request_timeout()
|
|
elif self.state == AWAIT_CONFIRMATION:
|
|
self.await_confirmation_timeout()
|
|
elif self.state == SEGMENTED_CONFIRMATION:
|
|
self.segmented_confirmation_timeout()
|
|
elif self.state == COMPLETED:
|
|
pass
|
|
elif self.state == ABORTED:
|
|
pass
|
|
else:
|
|
e = RuntimeError("invalid state")
|
|
ClientSSM._exception("exception: %r", e)
|
|
raise e
|
|
|
|
def abort(self, reason):
|
|
"""This function is called when the transaction should be aborted."""
|
|
if _debug: ClientSSM._debug("abort %r", reason)
|
|
|
|
# change the state to aborted
|
|
self.set_state(ABORTED)
|
|
|
|
# build an abort PDU to return
|
|
abort_pdu = AbortPDU(False, self.invokeID, reason)
|
|
|
|
# return it
|
|
return abort_pdu
|
|
|
|
def segmented_request(self, apdu):
|
|
"""This function is called when the client is sending a segmented request
|
|
and receives an apdu."""
|
|
if _debug: ClientSSM._debug("segmented_request %r", apdu)
|
|
|
|
# client is ready for the next segment
|
|
if apdu.apduType == SegmentAckPDU.pduType:
|
|
if _debug: ClientSSM._debug(" - segment ack")
|
|
|
|
# duplicate ack received?
|
|
if not self.in_window(apdu.apduSeq, self.initialSequenceNumber):
|
|
if _debug: ClientSSM._debug(" - not in window")
|
|
self.restart_timer(self.ssmSAP.segmentTimeout)
|
|
|
|
# final ack received?
|
|
elif self.sentAllSegments:
|
|
if _debug: ClientSSM._debug(" - all done sending request")
|
|
self.set_state(AWAIT_CONFIRMATION, self.ssmSAP.retryTimeout)
|
|
|
|
# more segments to send
|
|
else:
|
|
if _debug: ClientSSM._debug(" - more segments to send")
|
|
|
|
self.initialSequenceNumber = (apdu.apduSeq + 1) % 256
|
|
self.actualWindowSize = apdu.apduWin
|
|
self.segmentRetryCount = 0
|
|
self.FillWindow(self.initialSequenceNumber)
|
|
self.restart_timer(self.ssmSAP.segmentTimeout)
|
|
|
|
# simple ack
|
|
elif (apdu.apduType == SimpleAckPDU.pduType):
|
|
if _debug: ClientSSM._debug(" - simple ack")
|
|
|
|
if not self.sentAllSegments:
|
|
abort = self.abort(AbortReason.invalidApduInThisState)
|
|
self.request(abort) # send it to the device
|
|
self.response(abort) # send it to the application
|
|
else:
|
|
self.set_state(COMPLETED)
|
|
self.response(apdu)
|
|
|
|
elif (apdu.apduType == ComplexAckPDU.pduType):
|
|
if _debug: ClientSSM._debug(" - complex ack")
|
|
|
|
if not self.sentAllSegments:
|
|
abort = self.abort(AbortReason.invalidApduInThisState)
|
|
self.request(abort) # send it to the device
|
|
self.response(abort) # send it to the application
|
|
|
|
elif not apdu.apduSeg:
|
|
self.set_state(COMPLETED)
|
|
self.response(apdu)
|
|
|
|
else:
|
|
# set the segmented response context
|
|
self.set_segmentation_context(apdu)
|
|
|
|
self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted)
|
|
self.lastSequenceNumber = 0
|
|
self.initialSequenceNumber = 0
|
|
self.set_state(SEGMENTED_CONFIRMATION, self.ssmSAP.segmentTimeout)
|
|
|
|
# some kind of problem
|
|
elif (apdu.apduType == ErrorPDU.pduType) or (apdu.apduType == RejectPDU.pduType) or (apdu.apduType == AbortPDU.pduType):
|
|
if _debug: ClientSSM._debug(" - error/reject/abort")
|
|
|
|
self.set_state(COMPLETED)
|
|
self.response = apdu
|
|
self.response(apdu)
|
|
|
|
else:
|
|
raise RuntimeError("invalid APDU (2)")
|
|
|
|
def segmented_request_timeout(self):
|
|
if _debug: ClientSSM._debug("segmented_request_timeout")
|
|
|
|
# try again
|
|
if self.segmentRetryCount < self.ssmSAP.retryCount:
|
|
if _debug: ClientSSM._debug(" - retry segmented request")
|
|
|
|
self.segmentRetryCount += 1
|
|
self.start_timer(self.ssmSAP.segmentTimeout)
|
|
self.FillWindow(self.initialSequenceNumber)
|
|
else:
|
|
if _debug: ClientSSM._debug(" - abort, no response from the device")
|
|
|
|
abort = self.abort(AbortReason.noResponse)
|
|
self.response(abort)
|
|
|
|
def await_confirmation(self, apdu):
|
|
if _debug: ClientSSM._debug("await_confirmation %r", apdu)
|
|
|
|
if (apdu.apduType == AbortPDU.pduType):
|
|
if _debug: ClientSSM._debug(" - server aborted")
|
|
|
|
self.set_state(ABORTED)
|
|
self.response(apdu)
|
|
|
|
elif (apdu.apduType == SimpleAckPDU.pduType) or (apdu.apduType == ErrorPDU.pduType) or (apdu.apduType == RejectPDU.pduType):
|
|
if _debug: ClientSSM._debug(" - simple ack, error, or reject")
|
|
|
|
self.set_state(COMPLETED)
|
|
self.response(apdu)
|
|
|
|
elif (apdu.apduType == ComplexAckPDU.pduType):
|
|
if _debug: ClientSSM._debug(" - complex ack")
|
|
|
|
# if the response is not segmented, we're done
|
|
if not apdu.apduSeg:
|
|
if _debug: ClientSSM._debug(" - unsegmented")
|
|
|
|
self.set_state(COMPLETED)
|
|
self.response(apdu)
|
|
|
|
elif self.ssmSAP.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'):
|
|
if _debug: ClientSSM._debug(" - local device can't receive segmented messages")
|
|
abort = self.abort(AbortReason.segmentationNotSupported)
|
|
self.response(abort)
|
|
|
|
elif apdu.apduSeq == 0:
|
|
if _debug: ClientSSM._debug(" - segmented response")
|
|
|
|
# set the segmented response context
|
|
self.set_segmentation_context(apdu)
|
|
|
|
self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted)
|
|
self.lastSequenceNumber = 0
|
|
self.initialSequenceNumber = 0
|
|
self.set_state(SEGMENTED_CONFIRMATION, self.ssmSAP.segmentTimeout)
|
|
|
|
# send back a segment ack
|
|
segack = SegmentAckPDU( 0, 0, self.invokeID, self.initialSequenceNumber, self.actualWindowSize )
|
|
self.request(segack)
|
|
|
|
else:
|
|
if _debug: ClientSSM._debug(" - invalid APDU in this state")
|
|
|
|
abort = self.abort(AbortReason.invalidApduInThisState)
|
|
self.request(abort) # send it to the device
|
|
self.response(abort) # send it to the application
|
|
|
|
elif (apdu.apduType == SegmentAckPDU.pduType):
|
|
if _debug: ClientSSM._debug(" - segment ack(!?)")
|
|
|
|
self.restart_timer(self.ssmSAP.segmentTimeout)
|
|
|
|
else:
|
|
raise RuntimeError("invalid APDU (3)")
|
|
|
|
def await_confirmation_timeout(self):
|
|
if _debug: ClientSSM._debug("await_confirmation_timeout")
|
|
|
|
self.retryCount += 1
|
|
if self.retryCount < self.ssmSAP.retryCount:
|
|
if _debug: ClientSSM._debug(" - no response, try again (%d < %d)", self.retryCount, self.ssmSAP.retryCount)
|
|
|
|
# save the retry count, indication acts like the request is coming
|
|
# from the application so the retryCount gets re-initialized.
|
|
saveCount = self.retryCount
|
|
self.indication(self.segmentAPDU)
|
|
self.retryCount = saveCount
|
|
else:
|
|
if _debug: ClientSSM._debug(" - retry count exceeded")
|
|
abort = self.abort(AbortReason.noResponse)
|
|
self.response(abort)
|
|
|
|
def segmented_confirmation(self, apdu):
|
|
if _debug: ClientSSM._debug("segmented_confirmation %r", apdu)
|
|
|
|
# the only messages we should be getting are complex acks
|
|
if (apdu.apduType != ComplexAckPDU.pduType):
|
|
if _debug: ClientSSM._debug(" - complex ack required")
|
|
|
|
abort = self.abort(AbortReason.invalidApduInThisState)
|
|
self.request(abort) # send it to the device
|
|
self.response(abort) # send it to the application
|
|
return
|
|
|
|
# it must be segmented
|
|
if not apdu.apduSeg:
|
|
if _debug: ClientSSM._debug(" - must be segmented")
|
|
|
|
abort = self.abort(AbortReason.invalidApduInThisState)
|
|
self.request(abort) # send it to the device
|
|
self.response(abort) # send it to the application
|
|
return
|
|
|
|
# proper segment number
|
|
if apdu.apduSeq != (self.lastSequenceNumber + 1) % 256:
|
|
if _debug: ClientSSM._debug(" - segment %s received out of order, should be %s", apdu.apduSeq, (self.lastSequenceNumber + 1) % 256)
|
|
|
|
# segment received out of order
|
|
self.restart_timer(self.ssmSAP.segmentTimeout)
|
|
segack = SegmentAckPDU( 1, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize )
|
|
self.request(segack)
|
|
return
|
|
|
|
# add the data
|
|
self.append_segment(apdu)
|
|
|
|
# update the sequence number
|
|
self.lastSequenceNumber = (self.lastSequenceNumber + 1) % 256
|
|
|
|
# last segment received
|
|
if not apdu.apduMor:
|
|
if _debug: ClientSSM._debug(" - no more follows")
|
|
|
|
# send a final ack
|
|
segack = SegmentAckPDU( 0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize )
|
|
self.request(segack)
|
|
|
|
self.set_state(COMPLETED)
|
|
self.response(self.segmentAPDU)
|
|
|
|
elif apdu.apduSeq == ((self.initialSequenceNumber + self.actualWindowSize) % 256):
|
|
if _debug: ClientSSM._debug(" - last segment in the group")
|
|
|
|
self.initialSequenceNumber = self.lastSequenceNumber
|
|
self.restart_timer(self.ssmSAP.segmentTimeout)
|
|
segack = SegmentAckPDU( 0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize )
|
|
self.request(segack)
|
|
|
|
else:
|
|
# wait for more segments
|
|
if _debug: ClientSSM._debug(" - wait for more segments")
|
|
|
|
self.restart_timer(self.ssmSAP.segmentTimeout)
|
|
|
|
def segmented_confirmation_timeout(self):
|
|
if _debug: ClientSSM._debug("segmented_confirmation_timeout")
|
|
|
|
abort = self.abort(AbortReason.noResponse)
|
|
self.response(abort)
|
|
|
|
bacpypes_debugging(ClientSSM)
|
|
|
|
#
|
|
# ServerSSM - Server Segmentation State Machine
|
|
#
|
|
|
|
class ServerSSM(SSM):
|
|
|
|
def __init__(self, sap, remoteDevice):
|
|
if _debug: ServerSSM._debug("__init__ %s %r", sap, remoteDevice)
|
|
SSM.__init__(self, sap, remoteDevice)
|
|
|
|
def set_state(self, newState, timer=0):
|
|
"""This function is called when the client wants to change state."""
|
|
if _debug: ServerSSM._debug("set_state %r (%s) timer=%r", newState, SSM.transactionLabels[newState], timer)
|
|
|
|
# do the regular state change
|
|
SSM.set_state(self, newState, timer)
|
|
|
|
# when completed or aborted, remove tracking
|
|
if (newState == COMPLETED) or (newState == ABORTED):
|
|
if _debug: ServerSSM._debug(" - remove from active transactions")
|
|
self.ssmSAP.serverTransactions.remove(self)
|
|
|
|
if _debug: ServerSSM._debug(" - release device information")
|
|
self.ssmSAP.deviceInfoCache.release_device_info(self.remoteDevice)
|
|
|
|
def request(self, apdu):
|
|
"""This function is called by transaction functions to send
|
|
to the application."""
|
|
if _debug: ServerSSM._debug("request %r", apdu)
|
|
|
|
# make sure it has a good source and destination
|
|
apdu.pduSource = self.remoteDevice.address
|
|
apdu.pduDestination = None
|
|
|
|
# send it via the device
|
|
self.ssmSAP.sap_request(apdu)
|
|
|
|
def indication(self, apdu):
|
|
"""This function is called for each downstream packet related to
|
|
the transaction."""
|
|
if _debug: ServerSSM._debug("indication %r", apdu)
|
|
|
|
if self.state == IDLE:
|
|
self.idle(apdu)
|
|
elif self.state == SEGMENTED_REQUEST:
|
|
self.segmented_request(apdu)
|
|
elif self.state == AWAIT_RESPONSE:
|
|
self.await_response(apdu)
|
|
elif self.state == SEGMENTED_RESPONSE:
|
|
self.segmented_response(apdu)
|
|
else:
|
|
if _debug: ServerSSM._debug(" - invalid state")
|
|
|
|
def response(self, apdu):
|
|
"""This function is called by transaction functions when they want
|
|
to send a message to the device."""
|
|
if _debug: ServerSSM._debug("response %r", apdu)
|
|
|
|
# make sure it has a good source and destination
|
|
apdu.pduSource = None
|
|
apdu.pduDestination = self.remoteDevice.address
|
|
|
|
# send it via the device
|
|
self.ssmSAP.request(apdu)
|
|
|
|
def confirmation(self, apdu):
|
|
"""This function is called when the application has provided a response
|
|
and needs it to be sent to the client."""
|
|
if _debug: ServerSSM._debug("confirmation %r", apdu)
|
|
|
|
# check to see we are in the correct state
|
|
if self.state != AWAIT_RESPONSE:
|
|
if _debug: ServerSSM._debug(" - warning: not expecting a response")
|
|
|
|
# abort response
|
|
if (apdu.apduType == AbortPDU.pduType):
|
|
if _debug: ServerSSM._debug(" - abort")
|
|
|
|
self.set_state(ABORTED)
|
|
|
|
# send the response to the device
|
|
self.response(apdu)
|
|
return
|
|
|
|
# simple response
|
|
if (apdu.apduType == SimpleAckPDU.pduType) or (apdu.apduType == ErrorPDU.pduType) or (apdu.apduType == RejectPDU.pduType):
|
|
if _debug: ServerSSM._debug(" - simple ack, error, or reject")
|
|
|
|
# transaction completed
|
|
self.set_state(COMPLETED)
|
|
|
|
# send the response to the device
|
|
self.response(apdu)
|
|
return
|
|
|
|
# complex ack
|
|
if (apdu.apduType == ComplexAckPDU.pduType):
|
|
if _debug: ServerSSM._debug(" - complex ack")
|
|
|
|
# save the response and set the segmentation context
|
|
self.set_segmentation_context(apdu)
|
|
|
|
# the segment size is the minimum of the maximum size I can transmit
|
|
# (assumed to have no local buffer limitations), the maximum conveyable
|
|
# by the internetwork to the remote device, and the maximum APDU size
|
|
# accepted by the remote device.
|
|
self.segmentSize = min(self.remoteDevice.maxNpduLength, self.remoteDevice.maxApduLengthAccepted)
|
|
if _debug: ServerSSM._debug(" - segment size: %r", self.segmentSize)
|
|
|
|
# compute the segment count ### minus the header?
|
|
if not apdu.pduData:
|
|
# always at least one segment
|
|
self.segmentCount = 1
|
|
else:
|
|
# split into chunks, maybe need one more
|
|
self.segmentCount, more = divmod(len(apdu.pduData), self.segmentSize)
|
|
if more:
|
|
self.segmentCount += 1
|
|
if _debug: ServerSSM._debug(" - segment count: %r", self.segmentCount)
|
|
|
|
# make sure we support segmented transmit if we need to
|
|
if self.segmentCount > 1:
|
|
if _debug: ServerSSM._debug(" - segmentation required, %d segments", self.segmentCount)
|
|
|
|
# make sure we support segmented transmit
|
|
if self.ssmSAP.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'):
|
|
if _debug: ServerSSM._debug(" - server can't send segmented responses")
|
|
abort = self.abort(AbortReason.segmentationNotSupported)
|
|
self.reponse(abort)
|
|
return
|
|
|
|
# make sure client supports segmented receive
|
|
if self.remoteDevice.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'):
|
|
if _debug: ServerSSM._debug(" - client can't receive segmented responses")
|
|
abort = self.abort(AbortReason.segmentationNotSupported)
|
|
self.response(abort)
|
|
return
|
|
|
|
### check for APDUTooLong?
|
|
|
|
# initialize the state
|
|
self.segmentRetryCount = 0
|
|
self.initialSequenceNumber = 0
|
|
self.proposedWindowSize = self.ssmSAP.maxSegmentsAccepted
|
|
self.actualWindowSize = 1
|
|
|
|
# send out the first segment (or the whole thing)
|
|
if self.segmentCount == 1:
|
|
self.response(apdu)
|
|
self.set_state(COMPLETED)
|
|
else:
|
|
self.response(self.get_segment(0))
|
|
self.set_state(SEGMENTED_RESPONSE, self.ssmSAP.segmentTimeout)
|
|
|
|
else:
|
|
raise RuntimeError("invalid APDU (4)")
|
|
|
|
def process_task(self):
|
|
"""This function is called when the client has failed to send all of the
|
|
segments of a segmented request, the application has taken too long to
|
|
complete the request, or the client failed to ack the segments of a
|
|
segmented response."""
|
|
if _debug: ServerSSM._debug("process_task")
|
|
|
|
if self.state == SEGMENTED_REQUEST:
|
|
self.segmented_request_timeout()
|
|
elif self.state == AWAIT_RESPONSE:
|
|
self.await_response_timeout()
|
|
elif self.state == SEGMENTED_RESPONSE:
|
|
self.segmented_response_timeout()
|
|
elif self.state == COMPLETED:
|
|
pass
|
|
elif self.state == ABORTED:
|
|
pass
|
|
else:
|
|
if _debug: ServerSSM._debug("invalid state")
|
|
raise RuntimeError("invalid state")
|
|
|
|
def abort(self, reason):
|
|
"""This function is called when the application would like to abort the
|
|
transaction. There is no notification back to the application."""
|
|
if _debug: ServerSSM._debug("abort %r", reason)
|
|
|
|
# change the state to aborted
|
|
self.set_state(ABORTED)
|
|
|
|
# return an abort APDU
|
|
return AbortPDU(True, self.invokeID, reason)
|
|
|
|
def idle(self, apdu):
|
|
if _debug: ServerSSM._debug("idle %r", apdu)
|
|
|
|
# make sure we're getting confirmed requests
|
|
if not isinstance(apdu, ConfirmedRequestPDU):
|
|
raise RuntimeError("invalid APDU (5)")
|
|
|
|
# save the invoke ID
|
|
self.invokeID = apdu.apduInvokeID
|
|
if _debug: ServerSSM._debug(" - invoke ID: %r", self.invokeID)
|
|
|
|
# make sure the device information is synced with the request
|
|
if apdu.apduSA:
|
|
if self.remoteDevice.segmentationSupported == 'noSegmentation':
|
|
if _debug: ServerSSM._debug(" - client actually supports segmented receive")
|
|
self.remoteDevice.segmentationSupported = 'segmentedReceive'
|
|
|
|
if _debug: ServerSSM._debug(" - tell the cache the info has been updated")
|
|
self.ssmSAP.deviceInfoCache.update_device_info(self.remoteDevice)
|
|
|
|
elif self.remoteDevice.segmentationSupported == 'segmentedTransmit':
|
|
if _debug: ServerSSM._debug(" - client actually supports both segmented transmit and receive")
|
|
self.remoteDevice.segmentationSupported = 'segmentedBoth'
|
|
|
|
if _debug: ServerSSM._debug(" - tell the cache the info has been updated")
|
|
self.ssmSAP.deviceInfoCache.update_device_info(self.remoteDevice)
|
|
|
|
elif self.remoteDevice.segmentationSupported == 'segmentedReceive':
|
|
pass
|
|
|
|
elif self.remoteDevice.segmentationSupported == 'segmentedBoth':
|
|
pass
|
|
|
|
else:
|
|
raise RuntimeError("invalid segmentation supported in device info")
|
|
|
|
if apdu.apduMaxSegs != self.remoteDevice.maxSegmentsAccepted:
|
|
if _debug: ServerSSM._debug(" - update maximum segments accepted?")
|
|
if apdu.apduMaxResp != self.remoteDevice.maxApduLengthAccepted:
|
|
if _debug: ServerSSM._debug(" - update maximum max APDU length accepted?")
|
|
|
|
# save the number of segments the client is willing to accept in the ack
|
|
self.maxSegmentsAccepted = apdu.apduMaxSegs
|
|
|
|
# unsegmented request
|
|
if not apdu.apduSeg:
|
|
self.set_state(AWAIT_RESPONSE, self.ssmSAP.applicationTimeout)
|
|
self.request(apdu)
|
|
return
|
|
|
|
# make sure we support segmented requests
|
|
if self.ssmSAP.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'):
|
|
abort = self.abort(AbortReason.segmentationNotSupported)
|
|
self.response(abort)
|
|
return
|
|
|
|
# save the request and set the segmentation context
|
|
self.set_segmentation_context(apdu)
|
|
|
|
# the window size is the minimum of what I'm willing to receive and
|
|
# what the device has said it would like to send
|
|
self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted)
|
|
|
|
# initialize the state
|
|
self.lastSequenceNumber = 0
|
|
self.initialSequenceNumber = 0
|
|
self.set_state(SEGMENTED_REQUEST, self.ssmSAP.segmentTimeout)
|
|
|
|
# send back a segment ack
|
|
segack = SegmentAckPDU( 0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize )
|
|
if _debug: ServerSSM._debug(" - segAck: %r", segack)
|
|
|
|
self.response(segack)
|
|
|
|
def segmented_request(self, apdu):
|
|
if _debug: ServerSSM._debug("segmented_request %r", apdu)
|
|
|
|
# some kind of problem
|
|
if (apdu.apduType == AbortPDU.pduType):
|
|
self.set_state(COMPLETED)
|
|
self.response(apdu)
|
|
return
|
|
|
|
# the only messages we should be getting are confirmed requests
|
|
elif (apdu.apduType != ConfirmedRequestPDU.pduType):
|
|
abort = self.abort(AbortReason.invalidApduInThisState)
|
|
self.request(abort) # send it to the device
|
|
self.response(abort) # send it to the application
|
|
return
|
|
|
|
# it must be segmented
|
|
elif not apdu.apduSeg:
|
|
abort = self.abort(AbortReason.invalidApduInThisState)
|
|
self.request(abort) # send it to the application
|
|
self.response(abort) # send it to the device
|
|
return
|
|
|
|
# proper segment number
|
|
if apdu.apduSeq != (self.lastSequenceNumber + 1) % 256:
|
|
if _debug: ServerSSM._debug(" - segment %d received out of order, should be %d", apdu.apduSeq, (self.lastSequenceNumber + 1) % 256)
|
|
|
|
# segment received out of order
|
|
self.restart_timer(self.ssmSAP.segmentTimeout)
|
|
|
|
# send back a segment ack
|
|
segack = SegmentAckPDU( 1, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize )
|
|
|
|
self.response(segack)
|
|
return
|
|
|
|
# add the data
|
|
self.append_segment(apdu)
|
|
|
|
# update the sequence number
|
|
self.lastSequenceNumber = (self.lastSequenceNumber + 1) % 256
|
|
|
|
# last segment?
|
|
if not apdu.apduMor:
|
|
if _debug: ServerSSM._debug(" - no more follows")
|
|
|
|
# send back a final segment ack
|
|
segack = SegmentAckPDU( 0, 1, self.invokeID, self.lastSequenceNumber, self.actualWindowSize )
|
|
self.response(segack)
|
|
|
|
# forward the whole thing to the application
|
|
self.set_state(AWAIT_RESPONSE, self.ssmSAP.applicationTimeout)
|
|
self.request(self.segmentAPDU)
|
|
|
|
elif apdu.apduSeq == ((self.initialSequenceNumber + self.actualWindowSize) % 256):
|
|
if _debug: ServerSSM._debug(" - last segment in the group")
|
|
|
|
self.initialSequenceNumber = self.lastSequenceNumber
|
|
self.restart_timer(self.ssmSAP.segmentTimeout)
|
|
|
|
# send back a segment ack
|
|
segack = SegmentAckPDU( 0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize )
|
|
self.response(segack)
|
|
|
|
else:
|
|
# wait for more segments
|
|
if _debug: ServerSSM._debug(" - wait for more segments")
|
|
|
|
self.restart_timer(self.ssmSAP.segmentTimeout)
|
|
|
|
def segmented_request_timeout(self):
|
|
if _debug: ServerSSM._debug("segmented_request_timeout")
|
|
|
|
# give up
|
|
self.set_state(ABORTED)
|
|
|
|
def await_response(self, apdu):
|
|
if _debug: ServerSSM._debug("await_response %r", apdu)
|
|
|
|
if isinstance(apdu, ConfirmedRequestPDU):
|
|
if _debug: ServerSSM._debug(" - client is trying this request again")
|
|
|
|
elif isinstance(apdu, AbortPDU):
|
|
if _debug: ServerSSM._debug(" - client aborting this request")
|
|
|
|
# forward abort to the application
|
|
self.set_state(ABORTED)
|
|
self.request(apdu)
|
|
|
|
else:
|
|
raise RuntimeError("invalid APDU (6)")
|
|
|
|
def await_response_timeout(self):
|
|
"""This function is called when the application has taken too long
|
|
to respond to a clients request. The client has probably long since
|
|
given up."""
|
|
if _debug: ServerSSM._debug("await_response_timeout")
|
|
|
|
abort = self.abort(AbortReason.serverTimeout)
|
|
self.request(abort)
|
|
|
|
def segmented_response(self, apdu):
|
|
if _debug: ServerSSM._debug("segmented_response %r", apdu)
|
|
|
|
# client is ready for the next segment
|
|
if (apdu.apduType == SegmentAckPDU.pduType):
|
|
if _debug: ServerSSM._debug(" - segment ack")
|
|
|
|
# duplicate ack received?
|
|
if not self.in_window(apdu.apduSeq, self.initialSequenceNumber):
|
|
if _debug: ServerSSM._debug(" - not in window")
|
|
self.restart_timer(self.ssmSAP.segmentTimeout)
|
|
|
|
# final ack received?
|
|
elif self.sentAllSegments:
|
|
if _debug: ServerSSM._debug(" - all done sending response")
|
|
self.set_state(COMPLETED)
|
|
|
|
else:
|
|
if _debug: ServerSSM._debug(" - more segments to send")
|
|
|
|
self.initialSequenceNumber = (apdu.apduSeq + 1) % 256
|
|
self.actualWindowSize = apdu.apduWin
|
|
self.segmentRetryCount = 0
|
|
self.FillWindow(self.initialSequenceNumber)
|
|
self.restart_timer(self.ssmSAP.segmentTimeout)
|
|
|
|
# some kind of problem
|
|
elif (apdu.apduType == AbortPDU.pduType):
|
|
self.set_state(COMPLETED)
|
|
self.response(apdu)
|
|
|
|
else:
|
|
raise RuntimeError("invalid APDU (7)")
|
|
|
|
def segmented_response_timeout(self):
|
|
if _debug: ServerSSM._debug("segmented_response_timeout")
|
|
|
|
# try again
|
|
if self.segmentRetryCount < self.ssmSAP.retryCount:
|
|
self.segmentRetryCount += 1
|
|
self.start_timer(self.ssmSAP.segmentTimeout)
|
|
self.FillWindow(self.initialSequenceNumber)
|
|
else:
|
|
# give up
|
|
self.set_state(ABORTED)
|
|
|
|
bacpypes_debugging(ServerSSM)
|
|
|
|
#
|
|
# StateMachineAccessPoint
|
|
#
|
|
|
|
class StateMachineAccessPoint(Client, ServiceAccessPoint):
|
|
|
|
def __init__(self, localDevice=None, deviceInfoCache=None, sap=None, cid=None):
|
|
if _debug: StateMachineAccessPoint._debug("__init__ localDevice=%r deviceInfoCache=%r sap=%r cid=%r", localDevice, deviceInfoCache, sap, cid)
|
|
|
|
# basic initialization
|
|
Client.__init__(self, cid)
|
|
ServiceAccessPoint.__init__(self, sap)
|
|
|
|
# save a reference to the device information cache
|
|
self.deviceInfoCache = deviceInfoCache
|
|
|
|
# client settings
|
|
self.nextInvokeID = 1
|
|
self.clientTransactions = []
|
|
|
|
# server settings
|
|
self.serverTransactions = []
|
|
|
|
# confirmed request defaults
|
|
self.retryCount = 3
|
|
self.retryTimeout = 3000
|
|
self.maxApduLengthAccepted = 1024
|
|
|
|
# segmentation defaults
|
|
self.segmentationSupported = 'noSegmentation'
|
|
self.segmentTimeout = 1500
|
|
self.maxSegmentsAccepted = 8
|
|
|
|
# device communication control
|
|
self.dccEnableDisable = 'enable'
|
|
|
|
# local device object provides these
|
|
if localDevice:
|
|
self.retryCount = localDevice.numberOfApduRetries
|
|
self.retryTimeout = localDevice.apduTimeout
|
|
self.segmentationSupported = localDevice.segmentationSupported
|
|
self.segmentTimeout = localDevice.apduSegmentTimeout
|
|
self.maxSegmentsAccepted = localDevice.maxSegmentsAccepted
|
|
self.maxApduLengthAccepted = localDevice.maxApduLengthAccepted
|
|
|
|
# how long the state machine is willing to wait for the application
|
|
# layer to form a response and send it
|
|
self.applicationTimeout = 3000
|
|
|
|
def get_next_invoke_id(self, addr):
|
|
"""Called by clients to get an unused invoke ID."""
|
|
if _debug: StateMachineAccessPoint._debug("get_next_invoke_id")
|
|
|
|
initialID = self.nextInvokeID
|
|
while 1:
|
|
invokeID = self.nextInvokeID
|
|
self.nextInvokeID = (self.nextInvokeID + 1) % 256
|
|
|
|
# see if we've checked for them all
|
|
if initialID == self.nextInvokeID:
|
|
raise RuntimeError("no available invoke ID")
|
|
|
|
for tr in self.clientTransactions:
|
|
if (invokeID == tr.invokeID) and (addr == tr.remoteDevice.address):
|
|
break
|
|
else:
|
|
break
|
|
|
|
return invokeID
|
|
|
|
def confirmation(self, pdu):
|
|
"""Packets coming up the stack are APDU's."""
|
|
if _debug: StateMachineAccessPoint._debug("confirmation %r", pdu)
|
|
|
|
# check device communication control
|
|
if self.dccEnableDisable == 'enable':
|
|
if _debug: StateMachineAccessPoint._debug(" - communications enabled")
|
|
elif self.dccEnableDisable == 'disable':
|
|
if (pdu.apduType == 0) and (pdu.apduService == 17):
|
|
if _debug: StateMachineAccessPoint._debug(" - continue with DCC request")
|
|
elif (pdu.apduType == 0) and (pdu.apduService == 20):
|
|
if _debug: StateMachineAccessPoint._debug(" - continue with reinitialize device")
|
|
elif (pdu.apduType == 1) and (pdu.apduService == 8):
|
|
if _debug: StateMachineAccessPoint._debug(" - continue with Who-Is")
|
|
else:
|
|
if _debug: StateMachineAccessPoint._debug(" - not a Who-Is, dropped")
|
|
return
|
|
elif self.dccEnableDisable == 'disableInitiation':
|
|
if _debug: StateMachineAccessPoint._debug(" - initiation disabled")
|
|
|
|
# make a more focused interpretation
|
|
atype = apdu_types.get(pdu.apduType)
|
|
if not atype:
|
|
StateMachineAccessPoint._warning(" - unknown apduType: %r", pdu.apduType)
|
|
return
|
|
|
|
# decode it
|
|
apdu = atype()
|
|
apdu.decode(pdu)
|
|
if _debug: StateMachineAccessPoint._debug(" - apdu: %r", apdu)
|
|
|
|
if isinstance(apdu, ConfirmedRequestPDU):
|
|
# find duplicates of this request
|
|
for tr in self.serverTransactions:
|
|
if (apdu.pduSource == tr.remoteDevice.address) and (apdu.apduInvokeID == tr.invokeID):
|
|
break
|
|
else:
|
|
# find the remote device information
|
|
remoteDevice = self.deviceInfoCache.get_device_info(apdu.pduSource)
|
|
|
|
# build a server transaction
|
|
tr = ServerSSM(self, remoteDevice)
|
|
|
|
# add it to our transactions to track it
|
|
self.serverTransactions.append(tr)
|
|
|
|
# let it run with the apdu
|
|
tr.indication(apdu)
|
|
|
|
elif isinstance(apdu, UnconfirmedRequestPDU):
|
|
# deliver directly to the application
|
|
self.sap_request(apdu)
|
|
|
|
elif isinstance(apdu, SimpleAckPDU) \
|
|
or isinstance(apdu, ComplexAckPDU) \
|
|
or isinstance(apdu, ErrorPDU) \
|
|
or isinstance(apdu, RejectPDU):
|
|
|
|
# find the client transaction this is acking
|
|
for tr in self.clientTransactions:
|
|
if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address):
|
|
break
|
|
else:
|
|
return
|
|
|
|
# send the packet on to the transaction
|
|
tr.confirmation(apdu)
|
|
|
|
elif isinstance(apdu, AbortPDU):
|
|
# find the transaction being aborted
|
|
if apdu.apduSrv:
|
|
for tr in self.clientTransactions:
|
|
if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address):
|
|
break
|
|
else:
|
|
return
|
|
|
|
# send the packet on to the transaction
|
|
tr.confirmation(apdu)
|
|
else:
|
|
for tr in self.serverTransactions:
|
|
if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address):
|
|
break
|
|
else:
|
|
return
|
|
|
|
# send the packet on to the transaction
|
|
tr.indication(apdu)
|
|
|
|
elif isinstance(apdu, SegmentAckPDU):
|
|
# find the transaction being aborted
|
|
if apdu.apduSrv:
|
|
for tr in self.clientTransactions:
|
|
if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address):
|
|
break
|
|
else:
|
|
return
|
|
|
|
# send the packet on to the transaction
|
|
tr.confirmation(apdu)
|
|
else:
|
|
for tr in self.serverTransactions:
|
|
if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address):
|
|
break
|
|
else:
|
|
return
|
|
|
|
# send the packet on to the transaction
|
|
tr.indication(apdu)
|
|
|
|
else:
|
|
raise RuntimeError("invalid APDU (8)")
|
|
|
|
def sap_indication(self, apdu):
|
|
"""This function is called when the application is requesting
|
|
a new transaction as a client."""
|
|
if _debug: StateMachineAccessPoint._debug("sap_indication %r", apdu)
|
|
|
|
# check device communication control
|
|
if self.dccEnableDisable == 'enable':
|
|
if _debug: StateMachineAccessPoint._debug(" - communications enabled")
|
|
|
|
elif self.dccEnableDisable == 'disable':
|
|
if _debug: StateMachineAccessPoint._debug(" - communications disabled")
|
|
return
|
|
|
|
elif self.dccEnableDisable == 'disableInitiation':
|
|
if _debug: StateMachineAccessPoint._debug(" - initiation disabled")
|
|
|
|
if (apdu.apduType == 1) and (apdu.apduService == 0):
|
|
if _debug: StateMachineAccessPoint._debug(" - continue with I-Am")
|
|
else:
|
|
if _debug: StateMachineAccessPoint._debug(" - not an I-Am")
|
|
return
|
|
|
|
if isinstance(apdu, UnconfirmedRequestPDU):
|
|
# deliver to the device
|
|
self.request(apdu)
|
|
|
|
elif isinstance(apdu, ConfirmedRequestPDU):
|
|
# make sure it has an invoke ID
|
|
if apdu.apduInvokeID is None:
|
|
apdu.apduInvokeID = self.get_next_invoke_id(apdu.pduDestination)
|
|
else:
|
|
# verify the invoke ID isn't already being used
|
|
for tr in self.clientTransactions:
|
|
if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.remoteDevice.address):
|
|
raise RuntimeError("invoke ID in use")
|
|
|
|
# warning for bogus requests
|
|
if (apdu.pduDestination.addrType != Address.localStationAddr) and (apdu.pduDestination.addrType != Address.remoteStationAddr):
|
|
StateMachineAccessPoint._warning("%s is not a local or remote station", apdu.pduDestination)
|
|
|
|
# find the remote device information
|
|
remoteDevice = self.deviceInfoCache.get_device_info(apdu.pduDestination)
|
|
if _debug: StateMachineAccessPoint._debug(" - remoteDevice: %r", remoteDevice)
|
|
|
|
# create a client transaction state machine
|
|
tr = ClientSSM(self, remoteDevice)
|
|
if _debug: StateMachineAccessPoint._debug(" - client segmentation state machine: %r", tr)
|
|
|
|
# add it to our transactions to track it
|
|
self.clientTransactions.append(tr)
|
|
|
|
# let it run
|
|
tr.indication(apdu)
|
|
|
|
else:
|
|
raise RuntimeError("invalid APDU (9)")
|
|
|
|
def sap_confirmation(self, apdu):
|
|
"""This function is called when the application is responding
|
|
to a request, the apdu may be a simple ack, complex ack, error, reject or abort."""
|
|
if _debug: StateMachineAccessPoint._debug("sap_confirmation %r", apdu)
|
|
|
|
if isinstance(apdu, SimpleAckPDU) \
|
|
or isinstance(apdu, ComplexAckPDU) \
|
|
or isinstance(apdu, ErrorPDU) \
|
|
or isinstance(apdu, RejectPDU) \
|
|
or isinstance(apdu, AbortPDU):
|
|
# find the appropriate server transaction
|
|
for tr in self.serverTransactions:
|
|
if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.remoteDevice.address):
|
|
break
|
|
else:
|
|
return
|
|
|
|
# pass control to the transaction
|
|
tr.confirmation(apdu)
|
|
|
|
else:
|
|
raise RuntimeError("invalid APDU (10)")
|
|
|
|
bacpypes_debugging(StateMachineAccessPoint)
|
|
|
|
#
|
|
# ApplicationServiceAccessPoint
|
|
#
|
|
|
|
class ApplicationServiceAccessPoint(ApplicationServiceElement, ServiceAccessPoint):
|
|
|
|
def __init__(self, aseID=None, sapID=None):
|
|
if _debug: ApplicationServiceAccessPoint._debug("__init__ aseID=%r sapID=%r", aseID, sapID)
|
|
ApplicationServiceElement.__init__(self, aseID)
|
|
ServiceAccessPoint.__init__(self, sapID)
|
|
|
|
def indication(self, apdu):
|
|
if _debug: ApplicationServiceAccessPoint._debug("indication %r", apdu)
|
|
|
|
if isinstance(apdu, ConfirmedRequestPDU):
|
|
atype = confirmed_request_types.get(apdu.apduService)
|
|
if not atype:
|
|
if _debug: ApplicationServiceAccessPoint._debug(" - no confirmed request decoder")
|
|
return
|
|
|
|
# assume no errors found
|
|
error_found = None
|
|
|
|
try:
|
|
xpdu = atype()
|
|
xpdu.decode(apdu)
|
|
except RejectException, err:
|
|
ApplicationServiceAccessPoint._debug(" - decoding reject: %r", err)
|
|
error_found = err
|
|
except AbortException, err:
|
|
ApplicationServiceAccessPoint._debug(" - decoding abort: %r", err)
|
|
error_found = err
|
|
|
|
# no error so far, keep going
|
|
if not error_found:
|
|
if _debug: ApplicationServiceAccessPoint._debug(" - no decoding error")
|
|
|
|
try:
|
|
# forward the decoded packet
|
|
self.sap_request(xpdu)
|
|
except RejectException, err:
|
|
ApplicationServiceAccessPoint._debug(" - execution reject: %r", err)
|
|
error_found = err
|
|
except AbortException, err:
|
|
ApplicationServiceAccessPoint._debug(" - execution abort: %r", err)
|
|
error_found = err
|
|
|
|
# if there was an error, send it back to the client
|
|
if isinstance(error_found, RejectException):
|
|
if _debug: ApplicationServiceAccessPoint._debug(" - reject exception: %r", error_found)
|
|
|
|
reject_pdu = RejectPDU(reason=error_found.rejectReason)
|
|
reject_pdu.set_context(apdu)
|
|
if _debug: ApplicationServiceAccessPoint._debug(" - reject_pdu: %r", reject_pdu)
|
|
|
|
# send it to the client
|
|
self.response(reject_pdu)
|
|
|
|
elif isinstance(error_found, AbortException):
|
|
if _debug: ApplicationServiceAccessPoint._debug(" - abort exception: %r", error_found)
|
|
|
|
abort_pdu = AbortPDU(reason=error_found.abortReason)
|
|
abort_pdu.set_context(apdu)
|
|
if _debug: ApplicationServiceAccessPoint._debug(" - abort_pdu: %r", abort_pdu)
|
|
|
|
# send it to the client
|
|
self.response(abort_pdu)
|
|
|
|
elif isinstance(apdu, UnconfirmedRequestPDU):
|
|
atype = unconfirmed_request_types.get(apdu.apduService)
|
|
if not atype:
|
|
if _debug: ApplicationServiceAccessPoint._debug(" - no unconfirmed request decoder")
|
|
return
|
|
|
|
try:
|
|
xpdu = atype()
|
|
xpdu.decode(apdu)
|
|
except RejectException, err:
|
|
ApplicationServiceAccessPoint._debug(" - decoding reject: %r", err)
|
|
return
|
|
except AbortException, err:
|
|
ApplicationServiceAccessPoint._debug(" - decoding abort: %r", err)
|
|
return
|
|
|
|
try:
|
|
# forward the decoded packet
|
|
self.sap_request(xpdu)
|
|
except RejectException, err:
|
|
ApplicationServiceAccessPoint._debug(" - execution reject: %r", err)
|
|
except AbortException, err:
|
|
ApplicationServiceAccessPoint._debug(" - execution abort: %r", err)
|
|
|
|
else:
|
|
if _debug: ApplicationServiceAccessPoint._debug(" - unknown PDU type?!")
|
|
|
|
def sap_indication(self, apdu):
|
|
if _debug: ApplicationServiceAccessPoint._debug("sap_indication %r", apdu)
|
|
|
|
if isinstance(apdu, ConfirmedRequestPDU):
|
|
try:
|
|
xpdu = ConfirmedRequestPDU()
|
|
apdu.encode(xpdu)
|
|
apdu._xpdu = xpdu
|
|
except Exception, err:
|
|
ApplicationServiceAccessPoint._exception("confirmed request encoding error: %r", err)
|
|
return
|
|
|
|
elif isinstance(apdu, UnconfirmedRequestPDU):
|
|
try:
|
|
xpdu = UnconfirmedRequestPDU()
|
|
apdu.encode(xpdu)
|
|
apdu._xpdu = xpdu
|
|
except Exception, err:
|
|
ApplicationServiceAccessPoint._exception("unconfirmed request encoding error: %r", err)
|
|
return
|
|
|
|
else:
|
|
if _debug: ApplicationServiceAccessPoint._debug(" - unknown PDU type?!")
|
|
return
|
|
|
|
if _debug: ApplicationServiceAccessPoint._debug(" - xpdu %r", xpdu)
|
|
|
|
# forward the encoded packet
|
|
self.request(xpdu)
|
|
|
|
# if the upper layers of the application did not assign an invoke ID,
|
|
# copy the one that was assigned on its way down the stack
|
|
if isinstance(apdu, ConfirmedRequestPDU) and apdu.apduInvokeID is None:
|
|
if _debug: ApplicationServiceAccessPoint._debug(" - pass invoke ID upstream %r", xpdu.apduInvokeID)
|
|
apdu.apduInvokeID = xpdu.apduInvokeID
|
|
|
|
def confirmation(self, apdu):
|
|
if _debug: ApplicationServiceAccessPoint._debug("confirmation %r", apdu)
|
|
|
|
if isinstance(apdu, SimpleAckPDU):
|
|
xpdu = apdu
|
|
|
|
elif isinstance(apdu, ComplexAckPDU):
|
|
atype = complex_ack_types.get(apdu.apduService)
|
|
if not atype:
|
|
if _debug: ApplicationServiceAccessPoint._debug(" - no complex ack decoder")
|
|
return
|
|
|
|
try:
|
|
xpdu = atype()
|
|
xpdu.decode(apdu)
|
|
except Exception, err:
|
|
ApplicationServiceAccessPoint._exception("unconfirmed request decoding error: %r", err)
|
|
return
|
|
|
|
elif isinstance(apdu, ErrorPDU):
|
|
atype = error_types.get(apdu.apduService)
|
|
if not atype:
|
|
if _debug: ApplicationServiceAccessPoint._debug(" - no special error decoder")
|
|
atype = Error
|
|
|
|
try:
|
|
xpdu = atype()
|
|
xpdu.decode(apdu)
|
|
except Exception, err:
|
|
ApplicationServiceAccessPoint._exception("error PDU decoding error: %r", err)
|
|
xpdu = Error(errorClass=0, errorCode=0)
|
|
|
|
elif isinstance(apdu, RejectPDU):
|
|
xpdu = apdu
|
|
|
|
elif isinstance(apdu, AbortPDU):
|
|
xpdu = apdu
|
|
|
|
else:
|
|
if _debug: ApplicationServiceAccessPoint._debug(" - unknown PDU type")
|
|
return
|
|
|
|
if _debug: ApplicationServiceAccessPoint._debug(" - xpdu %r", xpdu)
|
|
|
|
# forward the decoded packet
|
|
self.sap_response(xpdu)
|
|
|
|
def sap_confirmation(self, apdu):
|
|
if _debug: ApplicationServiceAccessPoint._debug("sap_confirmation %r", apdu)
|
|
|
|
if isinstance(apdu, SimpleAckPDU):
|
|
xpdu = apdu
|
|
|
|
elif isinstance(apdu, ComplexAckPDU):
|
|
xpdu = ComplexAckPDU()
|
|
apdu.encode(xpdu)
|
|
|
|
elif isinstance(apdu, ErrorPDU):
|
|
xpdu = ErrorPDU()
|
|
apdu.encode(xpdu)
|
|
|
|
elif isinstance(apdu, RejectPDU):
|
|
xpdu = apdu
|
|
|
|
elif isinstance(apdu, AbortPDU):
|
|
xpdu = apdu
|
|
|
|
else:
|
|
if _debug: ApplicationServiceAccessPoint._debug(" - unknown PDU type")
|
|
return
|
|
|
|
if _debug: ApplicationServiceAccessPoint._debug(" - xpdu %r", xpdu)
|
|
|
|
# forward the encoded packet
|
|
self.response(xpdu)
|
|
|
|
bacpypes_debugging(ApplicationServiceAccessPoint)
|
|
|