diff --git a/py25/bacpypes/__init__.py b/py25/bacpypes/__init__.py index 70e98038..3adf9c1c 100755 --- a/py25/bacpypes/__init__.py +++ b/py25/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.17.0' +__version__ = '0.17.1' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py25/bacpypes/apdu.py b/py25/bacpypes/apdu.py index ae3659cf..118b4ab0 100755 --- a/py25/bacpypes/apdu.py +++ b/py25/bacpypes/apdu.py @@ -56,38 +56,51 @@ def register_error_type(klass): # encode_max_segments_accepted/decode_max_segments_accepted # +_max_segments_accepted_encoding = [ + None, 2, 4, 8, 16, 32, 64, None, + ] + def encode_max_segments_accepted(arg): """Encode the maximum number of segments the device will accept, Section - 20.1.2.4""" - w = 0 - while (arg and not arg & 1): - w += 1 - arg = (arg >> 1) - return w + 20.1.2.4, and if the device says it can only accept one segment it shouldn't + say that it supports segmentation!""" + # unspecified + if not arg: + return 0 + + if arg > 64: + return 7 + + # the largest number not greater than the arg + for i in range(6, 0, -1): + if _max_segments_accepted_encoding[i] <= arg: + return i + + raise ValueError("invalid max max segments accepted: {0}".format(arg)) def decode_max_segments_accepted(arg): """Decode the maximum number of segments the device will accept, Section 20.1.2.4""" - return arg and (1 << arg) or None + return _max_segments_accepted_encoding[arg] # # encode_max_apdu_length_accepted/decode_max_apdu_length_accepted # -_max_apdu_response_encoding = [50, 128, 206, 480, 1024, 1476, None, None, +_max_apdu_length_encoding = [50, 128, 206, 480, 1024, 1476, None, None, None, None, None, None, None, None, None, None] def encode_max_apdu_length_accepted(arg): """Return the encoding of the highest encodable value less than the value of the arg.""" for i in range(5, -1, -1): - if (arg >= _max_apdu_response_encoding[i]): + if (arg >= _max_apdu_length_encoding[i]): return i raise ValueError("invalid max APDU length accepted: {0}".format(arg)) def decode_max_apdu_length_accepted(arg): - v = _max_apdu_response_encoding[arg] + v = _max_apdu_length_encoding[arg] if not v: raise ValueError("invalid max APDU length accepted: {0}".format(arg)) @@ -173,7 +186,7 @@ def encode(self, pdu): if self.apduSA: buff += 0x02 pdu.put(buff) - pdu.put((encode_max_segments_accepted(self.apduMaxSegs) << 4) + encode_max_apdu_length_accepted(self.apduMaxResp)) + pdu.put((self.apduMaxSegs << 4) + self.apduMaxResp) pdu.put(self.apduInvokeID) if self.apduSeg: pdu.put(self.apduSeq) @@ -254,8 +267,8 @@ def decode(self, pdu): self.apduMor = ((buff & 0x04) != 0) self.apduSA = ((buff & 0x02) != 0) buff = pdu.get() - self.apduMaxSegs = decode_max_segments_accepted( (buff >> 4) & 0x07 ) - self.apduMaxResp = decode_max_apdu_length_accepted( buff & 0x0F ) + self.apduMaxSegs = (buff >> 4) & 0x07 + self.apduMaxResp = buff & 0x0F self.apduInvokeID = pdu.get() if self.apduSeg: self.apduSeq = pdu.get() @@ -693,6 +706,7 @@ def decode(self, apdu): # create a tag list and decode the rest of the data self._tag_list = TagList() self._tag_list.decode(apdu) + if _debug: APCISequence._debug(" - tag list: %r", self._tag_list) # pass the taglist to the Sequence for additional decoding Sequence.decode(self, self._tag_list) @@ -1307,7 +1321,7 @@ class LifeSafetyOperationRequest(ConfirmedRequestSequence): [ Element('requestingProcessIdentifier', Unsigned, 0) , Element('requestingSource', CharacterString, 1) , Element('request', LifeSafetyOperation, 2) - , Element('objectIdentifier', ObjectIdentifier, 3) + , Element('objectIdentifier', ObjectIdentifier, 3, True) ] register_confirmed_request_type(LifeSafetyOperationRequest) @@ -1491,7 +1505,7 @@ class RemoveListElementRequest(ConfirmedRequestSequence): sequenceElements = \ [ Element('objectIdentifier', ObjectIdentifier, 0) , Element('propertyIdentifier', PropertyIdentifier, 1) - , Element('propertyArrayIndex', Unsigned, 2) + , Element('propertyArrayIndex', Unsigned, 2, True) , Element('listOfElements', Any, 3) ] @@ -1501,9 +1515,9 @@ class RemoveListElementRequest(ConfirmedRequestSequence): class DeviceCommunicationControlRequestEnableDisable(Enumerated): enumerations = \ - { 'enable':0 - , 'disable':1 - , 'disableInitiation':2 + { 'enable': 0 + , 'disable': 1 + , 'disableInitiation': 2 } class DeviceCommunicationControlRequest(ConfirmedRequestSequence): diff --git a/py25/bacpypes/app.py b/py25/bacpypes/app.py index fa5f9ca5..9be79fb7 100755 --- a/py25/bacpypes/app.py +++ b/py25/bacpypes/app.py @@ -26,7 +26,8 @@ # for computing protocol services supported from .apdu import confirmed_request_types, unconfirmed_request_types, \ - ConfirmedServiceChoice, UnconfirmedServiceChoice + ConfirmedServiceChoice, UnconfirmedServiceChoice, \ + IAmRequest from .basetypes import ServicesSupported # basic services @@ -53,16 +54,16 @@ class DeviceInfo(DebugContents): 'maxSegmentsAccepted', ) - def __init__(self): + def __init__(self, device_identifier, address): # this information is from an IAmRequest - self.deviceIdentifier = None # device identifier - self.address = None # LocalStation or RemoteStation + self.deviceIdentifier = device_identifier + self.address = address + self.maxApduLengthAccepted = 1024 # maximum APDU device will accept self.segmentationSupported = 'noSegmentation' # normally no segmentation + self.maxSegmentsAccepted = None # None iff no segmentation self.vendorID = None # vendor identifier - - self.maxNpduLength = 1497 # maximum we can send in transit - self.maxSegmentsAccepted = None # value for proposed/actual window size + self.maxNpduLength = None # maximum we can send in transit (see 19.4) bacpypes_debugging(DeviceInfo) @@ -72,127 +73,134 @@ def __init__(self): class DeviceInfoCache: - def __init__(self): + def __init__(self, device_info_class=DeviceInfo): if _debug: DeviceInfoCache._debug("__init__") + # a little error checking + if not issubclass(device_info_class, DeviceInfo): + raise ValueError("not a DeviceInfo subclass: %r" % (device_info_class,)) + # empty cache self.cache = {} + # class for new records + self.device_info_class = device_info_class + def has_device_info(self, key): """Return true iff cache has information about the device.""" if _debug: DeviceInfoCache._debug("has_device_info %r", key) return key in self.cache - def add_device_info(self, apdu): + def iam_device_info(self, apdu): """Create a device information record based on the contents of an IAmRequest and put it in the cache.""" - if _debug: DeviceInfoCache._debug("add_device_info %r", apdu) + if _debug: DeviceInfoCache._debug("iam_device_info %r", apdu) - # get the existing cache record by identifier - info = self.get_device_info(apdu.iAmDeviceIdentifier[1]) - if _debug: DeviceInfoCache._debug(" - info: %r", info) + # make sure the apdu is an I-Am + if not isinstance(apdu, IAmRequest): + raise ValueError("not an IAmRequest: %r" % (apdu,)) - # update existing record - if info: - if (info.address == apdu.pduSource): - return + # get the device instance + device_instance = apdu.iAmDeviceIdentifier[1] - info.address = apdu.pduSource - else: - # get the existing record by address (creates a new record) - info = self.get_device_info(apdu.pduSource) - if _debug: DeviceInfoCache._debug(" - info: %r", info) + # get the existing cache record if it exists + device_info = self.cache.get(device_instance, None) - info.deviceIdentifier = apdu.iAmDeviceIdentifier[1] + # maybe there is a record for this address + if not device_info: + device_info = self.cache.get(apdu.pduSource, None) - # update the rest of the values - info.maxApduLengthAccepted = apdu.maxAPDULengthAccepted - info.segmentationSupported = apdu.segmentationSupported - info.vendorID = apdu.vendorID + # make a new one using the class provided + if not device_info: + device_info = self.device_info_class(device_instance, apdu.pduSource) - # say this is an updated record - self.update_device_info(info) + # jam in the correct values + device_info.deviceIdentifier = device_instance + device_info.address = apdu.pduSource + device_info.maxApduLengthAccepted = apdu.maxAPDULengthAccepted + device_info.segmentationSupported = apdu.segmentationSupported + device_info.vendorID = apdu.vendorID + + # tell the cache this is an updated record + self.update_device_info(device_info) def get_device_info(self, key): - """Return the known information about the device. If the key is the - address of an unknown device, build a generic device information record - add put it in the cache.""" if _debug: DeviceInfoCache._debug("get_device_info %r", key) - if isinstance(key, int): - current_info = self.cache.get(key, None) + # get the info if it's there + device_info = self.cache.get(key, None) + if _debug: DeviceInfoCache._debug(" - device_info: %r", device_info) - elif not isinstance(key, Address): - raise TypeError("key must be integer or an address") + return device_info - elif key.addrType not in (Address.localStationAddr, Address.remoteStationAddr): - raise TypeError("address must be a local or remote station") - - else: - current_info = self.cache.get(key, None) - if not current_info: - current_info = DeviceInfo() - current_info.address = key - current_info._cache_keys = (None, key) - current_info._ref_count = 1 - - self.cache[key] = current_info - else: - if _debug: DeviceInfoCache._debug(" - reference bump") - current_info._ref_count += 1 - - if _debug: DeviceInfoCache._debug(" - current_info: %r", current_info) - - return current_info - - def update_device_info(self, info): + def update_device_info(self, device_info): """The application has updated one or more fields in the device information record and the cache needs to be updated to reflect the changes. If this is a cached version of a persistent record then this is the opportunity to update the database.""" - if _debug: DeviceInfoCache._debug("update_device_info %r", info) + if _debug: DeviceInfoCache._debug("update_device_info %r", device_info) - cache_id, cache_address = info._cache_keys + # give this a reference count if it doesn't have one + if not hasattr(device_info, '_ref_count'): + device_info._ref_count = 0 - if (cache_id is not None) and (info.deviceIdentifier != cache_id): + # get the current keys + cache_id, cache_address = getattr(device_info, '_cache_keys', (None, None)) + + if (cache_id is not None) and (device_info.deviceIdentifier != cache_id): if _debug: DeviceInfoCache._debug(" - device identifier updated") # remove the old reference, add the new one del self.cache[cache_id] - self.cache[info.deviceIdentifier] = info - - cache_id = info.deviceIdentifier + self.cache[device_info.deviceIdentifier] = device_info - if (cache_address is not None) and (info.address != cache_address): + if (cache_address is not None) and (device_info.address != cache_address): if _debug: DeviceInfoCache._debug(" - device address updated") # remove the old reference, add the new one del self.cache[cache_address] - self.cache[info.address] = info - - cache_address = info.address + self.cache[device_info.address] = device_info # update the keys - info._cache_keys = (cache_id, cache_address) + device_info._cache_keys = (device_info.deviceIdentifier, device_info.address) + + def acquire(self, key): + """Return the known information about the device and mark the record + as being used by a segmenation state machine.""" + if _debug: DeviceInfoCache._debug("acquire %r", key) + + if isinstance(key, int): + device_info = self.cache.get(key, None) + + elif not isinstance(key, Address): + raise TypeError("key must be integer or an address") + + elif key.addrType not in (Address.localStationAddr, Address.remoteStationAddr): + raise TypeError("address must be a local or remote station") - def release_device_info(self, info): + else: + device_info = self.cache.get(key, None) + + if device_info: + if _debug: DeviceInfoCache._debug(" - reference bump") + device_info._ref_count += 1 + + if _debug: DeviceInfoCache._debug(" - device_info: %r", device_info) + + return device_info + + def release(self, device_info): """This function is called by the segmentation state machine when it has finished with the device information.""" - if _debug: DeviceInfoCache._debug("release_device_info %r", info) + if _debug: DeviceInfoCache._debug("release %r", device_info) # this information record might be used by more than one SSM - if info._ref_count > 1: - if _debug: DeviceInfoCache._debug(" - multiple references") - info._ref_count -= 1 - return + if device_info._ref_count == 0: + raise RuntimeError("reference count") - cache_id, cache_address = info._cache_keys - if cache_id is not None: - del self.cache[cache_id] - if cache_address is not None: - del self.cache[cache_address] - if _debug: DeviceInfoCache._debug(" - released") + # decrement the reference count + device_info._ref_count -= 1 bacpypes_debugging(DeviceInfoCache) @@ -619,3 +627,4 @@ def __init__(self, localAddress, eID=None): self.nsap.bind(self.bip) bacpypes_debugging(BIPNetworkApplication) + diff --git a/py25/bacpypes/appservice.py b/py25/bacpypes/appservice.py index 942424e4..b30a7e8a 100755 --- a/py25/bacpypes/appservice.py +++ b/py25/bacpypes/appservice.py @@ -12,12 +12,14 @@ from .task import OneShotTask from .pdu import Address -from .apdu import AbortPDU, AbortReason, ComplexAckPDU, \ +from .apdu import encode_max_segments_accepted, decode_max_segments_accepted, \ + encode_max_apdu_length_accepted, decode_max_apdu_length_accepted, \ + 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 +from .errors import RejectException, AbortException, UnrecognizedService # some debugging _debug = 0 @@ -44,19 +46,23 @@ class SSM(OneShotTask, DebugContents): , 'SEGMENTED_RESPONSE', 'SEGMENTED_CONFIRMATION', 'COMPLETED', 'ABORTED' ] - _debug_contents = ('ssmSAP', 'localDevice', 'remoteDevice', 'invokeID' + _debug_contents = ('ssmSAP', 'localDevice', 'device_info', 'invokeID' , 'state', 'segmentAPDU', 'segmentSize', 'segmentCount', 'maxSegmentsAccepted' , 'retryCount', 'segmentRetryCount', 'sentAllSegments', 'lastSequenceNumber' , 'initialSequenceNumber', 'actualWindowSize', 'proposedWindowSize' ) - def __init__(self, sap, remoteDevice): + def __init__(self, sap, pdu_address): """Common parts for client and server segmentation.""" - if _debug: SSM._debug("__init__ %r %r", sap, remoteDevice) + if _debug: SSM._debug("__init__ %r %r", sap, pdu_address) OneShotTask.__init__(self) self.ssmSAP = sap # service access point - self.remoteDevice = remoteDevice # remote device information, a DeviceInfo instance + + # save the address and get the device information + self.pdu_address = pdu_address + self.device_info = sap.deviceInfoCache.get_device_info(pdu_address) + self.invokeID = None # invoke ID self.state = IDLE # initial state @@ -70,11 +76,17 @@ def __init__(self, sap, remoteDevice): 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 + # local device object provides these or SAP provides defaults, make + # copies here so they are consistent throughout the transaction but + # they could change from one transaction to the next + self.numberOfApduRetries = getattr(sap.localDevice, 'numberOfApduRetries', sap.numberOfApduRetries) + self.apduTimeout = getattr(sap.localDevice, 'apduTimeout', sap.apduTimeout) + + self.segmentationSupported = getattr(sap.localDevice, 'segmentationSupported', sap.segmentationSupported) + self.segmentTimeout = getattr(sap.localDevice, 'segmentTimeout', sap.segmentTimeout) + self.maxSegmentsAccepted = getattr(sap.localDevice, 'maxSegmentsAccepted', sap.maxSegmentsAccepted) + self.maxApduLengthAccepted = getattr(sap.localDevice, 'maxApduLengthAccepted', sap.maxApduLengthAccepted) def start_timer(self, msecs): if _debug: SSM._debug("start_timer %r", msecs) @@ -145,19 +157,19 @@ def get_segment(self, indx): # check for invalid segment number if indx >= self.segmentCount: - raise RuntimeError("invalid segment number %r, APDU has %r segments" % (indx, self.segmentCount)) + raise RuntimeError("invalid segment number {0}, APDU has {1} segments".format(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; + segAPDU.apduMaxSegs = encode_max_segments_accepted(self.maxSegmentsAccepted) + segAPDU.apduMaxResp = encode_max_apdu_length_accepted(self.maxApduLengthAccepted) + segAPDU.apduInvokeID = self.invokeID # segmented response accepted? - segAPDU.apduSA = self.ssmSAP.segmentationSupported in ('segmentedReceive', 'segmentedBoth') + segAPDU.apduSA = self.segmentationSupported in ('segmentedReceive', 'segmentedBoth') if _debug: SSM._debug(" - segmented response accepted: %r", segAPDU.apduSA) elif self.segmentAPDU.apduType == ComplexAckPDU.pduType: @@ -171,14 +183,21 @@ def get_segment(self, indx): segAPDU.pduUserData = self.segmentAPDU.pduUserData # make sure the destination is set - segAPDU.pduDestination = self.remoteDevice.address + segAPDU.pduDestination = self.pdu_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 + + # first segment sends proposed window size, rest get actual + if indx == 0: + if _debug: SSM._debug(" - proposedWindowSize: %r", self.proposedWindowSize) + segAPDU.apduWin = self.proposedWindowSize + else: + if _debug: SSM._debug(" - actualWindowSize: %r", self.actualWindowSize) + segAPDU.apduWin = self.actualWindowSize else: segAPDU.apduSeg = False segAPDU.apduMor = False @@ -210,10 +229,11 @@ def in_window(self, seqA, seqB): return rslt - def FillWindow(self, seqNum): + def fill_window(self, seqNum): """This function sends all of the packets necessary to fill out the segmentation window.""" - if _debug: SSM._debug("FillWindow %r", seqNum) + if _debug: SSM._debug("fill_window %r", seqNum) + if _debug: SSM._debug(" - actualWindowSize: %r", self.actualWindowSize) for ix in range(self.actualWindowSize): apdu = self.get_segment(seqNum + ix) @@ -234,13 +254,18 @@ def FillWindow(self, seqNum): class ClientSSM(SSM): - def __init__(self, sap, remoteDevice): - if _debug: ClientSSM._debug("__init__ %s %r", sap, remoteDevice) - SSM.__init__(self, sap, remoteDevice) + def __init__(self, sap, pdu_address): + if _debug: ClientSSM._debug("__init__ %s %r", sap, pdu_address) + SSM.__init__(self, sap, pdu_address) # initialize the retry count self.retryCount = 0 + # acquire the device info + if self.device_info: + if _debug: ClientSSM._debug(" - acquire device information") + self.ssmSAP.deviceInfoCache.acquire(self.device_info) + 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) @@ -253,8 +278,10 @@ def set_state(self, newState, timer=0): 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) + # release the device info + if self.device_info: + if _debug: ClientSSM._debug(" - release device information") + self.ssmSAP.deviceInfoCache.release(self.device_info) def request(self, apdu): """This function is called by client transaction functions when it wants @@ -263,7 +290,7 @@ def request(self, apdu): # make sure it has a good source and destination apdu.pduSource = None - apdu.pduDestination = self.remoteDevice.address + apdu.pduDestination = self.pdu_address # send it via the device self.ssmSAP.request(apdu) @@ -280,26 +307,27 @@ def indication(self, apdu): # 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) + # if the max apdu length of the server isn't known, assume that it + # is the same size as our own and will be the segment size + if (not self.device_info) or (self.device_info.maxApduLengthAccepted is None): + self.segmentSize = self.maxApduLengthAccepted - # the maximum number of segments acceptable in the reply - if apdu.apduMaxSegs is not None: - # this request overrides the default - self.maxSegmentsAccepted = apdu.apduMaxSegs + # if the max npdu length of the server isn't known, assume that it + # is the same as the max apdu length accepted + elif self.device_info.maxNpduLength is None: + self.segmentSize = self.device_info.maxApduLengthAccepted + + # the segment size is the minimum of the size of the largest packet + # that can be delivered to the server and the largest it can accept + else: + self.segmentSize = min(self.device_info.maxNpduLength, self.device_info.maxApduLengthAccepted) + if _debug: ClientSSM._debug(" - segment size: %r", self.segmentSize) # save the invoke ID self.invokeID = apdu.apduInvokeID if _debug: ClientSSM._debug(" - invoke ID: %r", self.invokeID) - # compute the segment count ### minus the header? + # compute the segment count if not apdu.pduData: # always at least one segment self.segmentCount = 1 @@ -312,34 +340,49 @@ def indication(self, apdu): # make sure we support segmented transmit if we need to if self.segmentCount > 1: - if self.ssmSAP.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): + if self.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") + + if not self.device_info: + if _debug: ClientSSM._debug(" - no server info for segmentation support") + + elif self.device_info.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if _debug: ClientSSM._debug(" - server can't receive segmented requests") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return - ### check for APDUTooLong? + # make sure we dont exceed the number of segments in our request + # that the server said it was willing to accept + if not self.device_info: + if _debug: ClientSSM._debug(" - no server info for maximum number of segments") + + elif not self.device_info.maxSegmentsAccepted: + if _debug: ClientSSM._debug(" - server doesn't say maximum number of segments") + + elif self.segmentCount > self.device_info.maxSegmentsAccepted: + if _debug: ClientSSM._debug(" - server can't receive enough segments") + abort = self.abort(AbortReason.apduTooLong) + self.response(abort) + return # send out the first segment (or the whole thing) if self.segmentCount == 1: - # SendConfirmedUnsegmented + # unsegmented self.sentAllSegments = True self.retryCount = 0 - self.set_state(AWAIT_CONFIRMATION, self.ssmSAP.retryTimeout) + self.set_state(AWAIT_CONFIRMATION, self.apduTimeout) else: - # SendConfirmedSegmented + # segmented 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) + self.actualWindowSize = None # segment ack will set value + self.set_state(SEGMENTED_REQUEST, self.segmentTimeout) # deliver to the device self.request(self.get_segment(0)) @@ -350,7 +393,7 @@ def response(self, apdu): if _debug: ClientSSM._debug("response %r", apdu) # make sure it has a good source and destination - apdu.pduSource = self.remoteDevice.address + apdu.pduSource = self.pdu_address apdu.pduDestination = None # send it to the application @@ -407,29 +450,31 @@ def segmented_request(self, apdu): and receives an apdu.""" if _debug: ClientSSM._debug("segmented_request %r", apdu) - # client is ready for the next segment + # server is ready for the next segment if apdu.apduType == SegmentAckPDU.pduType: if _debug: ClientSSM._debug(" - segment ack") + # actual window size is provided by server + self.actualWindowSize = apdu.apduWin + # 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) + self.restart_timer(self.segmentTimeout) # final ack received? elif self.sentAllSegments: if _debug: ClientSSM._debug(" - all done sending request") - self.set_state(AWAIT_CONFIRMATION, self.ssmSAP.retryTimeout) + self.set_state(AWAIT_CONFIRMATION, self.apduTimeout) # 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) + self.fill_window(self.initialSequenceNumber) + self.restart_timer(self.segmentTimeout) # simple ack elif (apdu.apduType == SimpleAckPDU.pduType): @@ -452,6 +497,7 @@ def segmented_request(self, apdu): self.response(abort) # send it to the application elif not apdu.apduSeg: + # ack is not segmented self.set_state(COMPLETED) self.response(apdu) @@ -459,10 +505,11 @@ def segmented_request(self, apdu): # set the segmented response context self.set_segmentation_context(apdu) - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + # minimum of what the server is proposing and this client proposes + self.actualWindowSize = min(apdu.apduWin, self.proposedWindowSize) self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_CONFIRMATION, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_CONFIRMATION, self.segmentTimeout) # some kind of problem elif (apdu.apduType == ErrorPDU.pduType) or (apdu.apduType == RejectPDU.pduType) or (apdu.apduType == AbortPDU.pduType): @@ -479,12 +526,12 @@ def segmented_request_timeout(self): if _debug: ClientSSM._debug("segmented_request_timeout") # try again - if self.segmentRetryCount < self.ssmSAP.retryCount: + if self.segmentRetryCount < self.numberOfApduRetries: if _debug: ClientSSM._debug(" - retry segmented request") self.segmentRetryCount += 1 - self.start_timer(self.ssmSAP.segmentTimeout) - self.FillWindow(self.initialSequenceNumber) + self.start_timer(self.segmentTimeout) + self.fill_window(self.initialSequenceNumber) else: if _debug: ClientSSM._debug(" - abort, no response from the device") @@ -516,7 +563,7 @@ def await_confirmation(self, apdu): self.set_state(COMPLETED) self.response(apdu) - elif self.ssmSAP.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + elif self.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): if _debug: ClientSSM._debug(" - local device can't receive segmented messages") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) @@ -527,10 +574,10 @@ def await_confirmation(self, apdu): # set the segmented response context self.set_segmentation_context(apdu) - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + self.actualWindowSize = apdu.apduWin self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_CONFIRMATION, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_CONFIRMATION, self.segmentTimeout) # send back a segment ack segack = SegmentAckPDU( 0, 0, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) @@ -546,7 +593,7 @@ def await_confirmation(self, apdu): elif (apdu.apduType == SegmentAckPDU.pduType): if _debug: ClientSSM._debug(" - segment ack(!?)") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) else: raise RuntimeError("invalid APDU (3)") @@ -554,9 +601,9 @@ def await_confirmation(self, apdu): 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) + if self.retryCount < self.numberOfApduRetries: + if _debug: ClientSSM._debug(" - no response, try again (%d < %d)", self.retryCount, self.numberOfApduRetries) + self.retryCount += 1 # save the retry count, indication acts like the request is coming # from the application so the retryCount gets re-initialized. @@ -594,8 +641,8 @@ def segmented_confirmation(self, apdu): 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.restart_timer(self.segmentTimeout) + segack = SegmentAckPDU(1, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) return @@ -610,7 +657,7 @@ def segmented_confirmation(self, apdu): if _debug: ClientSSM._debug(" - no more follows") # send a final ack - segack = SegmentAckPDU( 0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) self.set_state(COMPLETED) @@ -620,15 +667,15 @@ def segmented_confirmation(self, apdu): 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.restart_timer(self.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) + self.restart_timer(self.segmentTimeout) def segmented_confirmation_timeout(self): if _debug: ClientSSM._debug("segmented_confirmation_timeout") @@ -644,9 +691,14 @@ def segmented_confirmation_timeout(self): class ServerSSM(SSM): - def __init__(self, sap, remoteDevice): - if _debug: ServerSSM._debug("__init__ %s %r", sap, remoteDevice) - SSM.__init__(self, sap, remoteDevice) + def __init__(self, sap, pdu_address): + if _debug: ServerSSM._debug("__init__ %s %r", sap, pdu_address) + SSM.__init__(self, sap, pdu_address) + + # acquire the device info + if self.device_info: + if _debug: ServerSSM._debug(" - acquire device information") + self.ssmSAP.deviceInfoCache.acquire(self.device_info) def set_state(self, newState, timer=0): """This function is called when the client wants to change state.""" @@ -660,8 +712,10 @@ def set_state(self, newState, timer=0): 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) + # release the device info + if self.device_info: + if _debug: ClientSSM._debug(" - release device information") + self.ssmSAP.deviceInfoCache.release(self.device_info) def request(self, apdu): """This function is called by transaction functions to send @@ -669,7 +723,7 @@ def request(self, apdu): if _debug: ServerSSM._debug("request %r", apdu) # make sure it has a good source and destination - apdu.pduSource = self.remoteDevice.address + apdu.pduSource = self.pdu_address apdu.pduDestination = None # send it via the device @@ -698,7 +752,7 @@ def response(self, apdu): # make sure it has a good source and destination apdu.pduSource = None - apdu.pduDestination = self.remoteDevice.address + apdu.pduDestination = self.pdu_address # send it via the device self.ssmSAP.request(apdu) @@ -740,14 +794,15 @@ def confirmation(self, apdu): # 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) + # the segment size is the minimum of the size of the largest packet + # that can be delivered to the client and the largest it can accept + if (not self.device_info) or (self.device_info.maxNpduLength is None): + self.segmentSize = self.maxApduLengthAccepted + else: + self.segmentSize = min(self.device_info.maxNpduLength, self.maxApduLengthAccepted) if _debug: ServerSSM._debug(" - segment size: %r", self.segmentSize) - # compute the segment count ### minus the header? + # compute the segment count if not apdu.pduData: # always at least one segment self.segmentCount = 1 @@ -763,26 +818,31 @@ def confirmation(self, apdu): 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 self.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): if _debug: ServerSSM._debug(" - server can't send segmented responses") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return # make sure client supports segmented receive - if self.remoteDevice.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if self.device_info.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? + # make sure we dont exceed the number of segments in our response + # that the device said it was willing to accept in the request + if self.segmentCount > self.maxSegmentsAccepted: + if _debug: ServerSSM._debug(" - client can't receive enough segments") + abort = self.abort(AbortReason.apduTooLong) + self.response(abort) + return # initialize the state self.segmentRetryCount = 0 self.initialSequenceNumber = 0 - self.proposedWindowSize = self.ssmSAP.maxSegmentsAccepted - self.actualWindowSize = 1 + self.actualWindowSize = None # send out the first segment (or the whole thing) if self.segmentCount == 1: @@ -790,7 +850,7 @@ def confirmation(self, apdu): self.set_state(COMPLETED) else: self.response(self.get_segment(0)) - self.set_state(SEGMENTED_RESPONSE, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_RESPONSE, self.segmentTimeout) else: raise RuntimeError("invalid APDU (4)") @@ -838,38 +898,48 @@ def idle(self, apdu): 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 not self.device_info: + if _debug: ServerSSM._debug(" - no client device info") + + elif self.device_info.segmentationSupported == 'noSegmentation': if _debug: ServerSSM._debug(" - client actually supports segmented receive") - self.remoteDevice.segmentationSupported = 'segmentedReceive' + self.device_info.segmentationSupported = 'segmentedReceive' if _debug: ServerSSM._debug(" - tell the cache the info has been updated") - self.ssmSAP.deviceInfoCache.update_device_info(self.remoteDevice) + self.ssmSAP.deviceInfoCache.update_device_info(self.device_info) - elif self.remoteDevice.segmentationSupported == 'segmentedTransmit': + elif self.device_info.segmentationSupported == 'segmentedTransmit': if _debug: ServerSSM._debug(" - client actually supports both segmented transmit and receive") - self.remoteDevice.segmentationSupported = 'segmentedBoth' + self.device_info.segmentationSupported = 'segmentedBoth' if _debug: ServerSSM._debug(" - tell the cache the info has been updated") - self.ssmSAP.deviceInfoCache.update_device_info(self.remoteDevice) + self.ssmSAP.deviceInfoCache.update_device_info(self.device_info) - elif self.remoteDevice.segmentationSupported == 'segmentedReceive': + elif self.device_info.segmentationSupported == 'segmentedReceive': pass - elif self.remoteDevice.segmentationSupported == 'segmentedBoth': + elif self.device_info.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?") + # decode the maximum that the client can receive in one APDU, and if + # there is a value in the device information then use that one because + # it came from reading device object property value or from an I-Am + # message that was received + self.maxApduLengthAccepted = decode_max_apdu_length_accepted(apdu.apduMaxResp) + if self.device_info and self.device_info.maxApduLengthAccepted is not None: + if self.device_info.maxApduLengthAccepted < self.maxApduLengthAccepted: + if _debug: ServerSSM._debug(" - apduMaxResp encoding error") + else: + self.maxApduLengthAccepted = self.device_info.maxApduLengthAccepted + if _debug: ServerSSM._debug(" - maxApduLengthAccepted: %r", self.maxApduLengthAccepted) - # save the number of segments the client is willing to accept in the ack - self.maxSegmentsAccepted = apdu.apduMaxSegs + # save the number of segments the client is willing to accept in the ack, + # if this is None then the value is unknown or more than 64 + self.maxSegmentsAccepted = decode_max_segments_accepted(apdu.apduMaxSegs) # unsegmented request if not apdu.apduSeg: @@ -878,7 +948,7 @@ def idle(self, apdu): return # make sure we support segmented requests - if self.ssmSAP.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if self.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return @@ -886,17 +956,18 @@ def idle(self, apdu): # 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) + # the window size is the minimum of what I would propose and what the + # device has proposed + self.actualWindowSize = min(apdu.apduWin, self.proposedWindowSize) + if _debug: ServerSSM._debug(" - actualWindowSize? min(%r, %r) -> %r", apdu.apduWin, self.proposedWindowSize, self.actualWindowSize) # initialize the state self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_REQUEST, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_REQUEST, self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) if _debug: ServerSSM._debug(" - segAck: %r", segack) self.response(segack) @@ -929,10 +1000,10 @@ def segmented_request(self, apdu): 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) + self.restart_timer(self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 1, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(1, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) self.response(segack) return @@ -948,7 +1019,7 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - no more follows") # send back a final segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.response(segack) # forward the whole thing to the application @@ -959,17 +1030,17 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - last segment in the group") self.initialSequenceNumber = self.lastSequenceNumber - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + 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) + self.restart_timer(self.segmentTimeout) def segmented_request_timeout(self): if _debug: ServerSSM._debug("segmented_request_timeout") @@ -1009,10 +1080,13 @@ def segmented_response(self, apdu): if (apdu.apduType == SegmentAckPDU.pduType): if _debug: ServerSSM._debug(" - segment ack") + # actual window size is provided by client + self.actualWindowSize = apdu.apduWin + # 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) + self.restart_timer(self.segmentTimeout) # final ack received? elif self.sentAllSegments: @@ -1025,8 +1099,8 @@ def segmented_response(self, apdu): self.initialSequenceNumber = (apdu.apduSeq + 1) % 256 self.actualWindowSize = apdu.apduWin self.segmentRetryCount = 0 - self.FillWindow(self.initialSequenceNumber) - self.restart_timer(self.ssmSAP.segmentTimeout) + self.fill_window(self.initialSequenceNumber) + self.restart_timer(self.segmentTimeout) # some kind of problem elif (apdu.apduType == AbortPDU.pduType): @@ -1040,10 +1114,10 @@ def segmented_response_timeout(self): if _debug: ServerSSM._debug("segmented_response_timeout") # try again - if self.segmentRetryCount < self.ssmSAP.retryCount: + if self.segmentRetryCount < self.numberOfApduRetries: self.segmentRetryCount += 1 - self.start_timer(self.ssmSAP.segmentTimeout) - self.FillWindow(self.initialSequenceNumber) + self.start_timer(self.segmentTimeout) + self.fill_window(self.initialSequenceNumber) else: # give up self.set_state(ABORTED) @@ -1064,6 +1138,7 @@ def __init__(self, localDevice=None, deviceInfoCache=None, sap=None, cid=None): ServiceAccessPoint.__init__(self, sap) # save a reference to the device information cache + self.localDevice = localDevice self.deviceInfoCache = deviceInfoCache # client settings @@ -1074,27 +1149,19 @@ def __init__(self, localDevice=None, deviceInfoCache=None, sap=None, cid=None): self.serverTransactions = [] # confirmed request defaults - self.retryCount = 3 - self.retryTimeout = 3000 + self.numberOfApduRetries = 3 + self.apduTimeout = 3000 self.maxApduLengthAccepted = 1024 # segmentation defaults self.segmentationSupported = 'noSegmentation' self.segmentTimeout = 1500 - self.maxSegmentsAccepted = 8 + self.maxSegmentsAccepted = 2 + self.proposedWindowSize = 2 # 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 @@ -1113,7 +1180,7 @@ def get_next_invoke_id(self, addr): raise RuntimeError("no available invoke ID") for tr in self.clientTransactions: - if (invokeID == tr.invokeID) and (addr == tr.remoteDevice.address): + if (invokeID == tr.invokeID) and (addr == tr.pdu_address): break else: break @@ -1154,14 +1221,11 @@ def confirmation(self, pdu): 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): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: - # find the remote device information - remoteDevice = self.deviceInfoCache.get_device_info(apdu.pduSource) - # build a server transaction - tr = ServerSSM(self, remoteDevice) + tr = ServerSSM(self, apdu.pduSource) # add it to our transactions to track it self.serverTransactions.append(tr) @@ -1180,7 +1244,7 @@ def confirmation(self, pdu): # find the client transaction this is acking for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1192,7 +1256,7 @@ def confirmation(self, pdu): # find the transaction being aborted if apdu.apduSrv: for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1201,7 +1265,7 @@ def confirmation(self, pdu): tr.confirmation(apdu) else: for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1213,7 +1277,7 @@ def confirmation(self, pdu): # find the transaction being aborted if apdu.apduSrv: for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1222,7 +1286,7 @@ def confirmation(self, pdu): tr.confirmation(apdu) else: for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1266,19 +1330,15 @@ def sap_indication(self, apdu): 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): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.pdu_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) + tr = ClientSSM(self, apdu.pduDestination) if _debug: StateMachineAccessPoint._debug(" - client segmentation state machine: %r", tr) # add it to our transactions to track it @@ -1302,7 +1362,7 @@ def sap_confirmation(self, apdu): 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): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.pdu_address): break else: return @@ -1330,23 +1390,26 @@ def indication(self, apdu): if _debug: ApplicationServiceAccessPoint._debug("indication %r", apdu) if isinstance(apdu, ConfirmedRequestPDU): + # assume no errors found + error_found = None + + # look up the class associated with the service 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 + error_found = UnrecognizedService() - 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: + 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: diff --git a/py25/bacpypes/bvllservice.py b/py25/bacpypes/bvllservice.py index 65ae6143..2588f4c0 100755 --- a/py25/bacpypes/bvllservice.py +++ b/py25/bacpypes/bvllservice.py @@ -546,33 +546,38 @@ def confirmation(self, pdu): return - elif isinstance(pdu, OriginalUnicastNPDU): + if isinstance(pdu, OriginalUnicastNPDU): # build a vanilla PDU xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) # send it upstream self.response(xpdu) - # check the BBMD registration status, we may not be registered - if self.registrationStatus != 0: - if _debug: BIPForeign._debug(" - packet dropped, unregistered") - return - - if isinstance(pdu, ReadBroadcastDistributionTableAck): - # send this to the service access point - self.sap_response(pdu) + elif isinstance(pdu, ForwardedNPDU): + # check the BBMD registration status, we may not be registered + if self.registrationStatus != 0: + if _debug: BIPForeign._debug(" - packet dropped, unregistered") + return - elif isinstance(pdu, ReadForeignDeviceTableAck): - # send this to the service access point - self.sap_response(pdu) + # make sure the forwarded PDU from the bbmd + if pdu.pduSource != self.bbmdAddress: + if _debug: BIPForeign._debug(" - packet dropped, not from the BBMD") + return - elif isinstance(pdu, ForwardedNPDU): # build a PDU with the source from the real source xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) # send it upstream self.response(xpdu) + elif isinstance(pdu, ReadBroadcastDistributionTableAck): + # send this to the service access point + self.sap_response(pdu) + + elif isinstance(pdu, ReadForeignDeviceTableAck): + # send this to the service access point + self.sap_response(pdu) + elif isinstance(pdu, WriteBroadcastDistributionTable): # build a response xpdu = Result(code=0x0010, user_data=pdu.pduUserData) @@ -621,6 +626,9 @@ def confirmation(self, pdu): # send it downstream self.request(xpdu) + elif isinstance(pdu, OriginalBroadcastNPDU): + if _debug: BIPForeign._debug(" - packet dropped") + else: BIPForeign._warning("invalid pdu type: %s", type(pdu)) @@ -696,7 +704,7 @@ def indication(self, pdu): # make an original unicast PDU xpdu = OriginalUnicastNPDU(pdu, user_data=pdu.pduUserData) xpdu.pduDestination = pdu.pduDestination - if _debug: BIPBBMD._debug(" - xpdu: %r", xpdu) + if _debug: BIPBBMD._debug(" - original unicast xpdu: %r", xpdu) # send it downstream self.request(xpdu) @@ -719,13 +727,13 @@ def indication(self, pdu): for bdte in self.bbmdBDT: if bdte != self.bbmdAddress: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) else: @@ -741,8 +749,9 @@ def confirmation(self, pdu): elif isinstance(pdu, WriteBroadcastDistributionTable): # build a response - xpdu = Result(code=99, user_data=pdu.pduUserData) + xpdu = Result(code=0x0010, user_data=pdu.pduUserData) xpdu.pduDestination = pdu.pduSource + if _debug: BIPBBMD._debug(" - xpdu: %r", xpdu) # send it downstream self.request(xpdu) @@ -761,27 +770,38 @@ def confirmation(self, pdu): self.sap_response(pdu) elif isinstance(pdu, ForwardedNPDU): - # build a PDU with the source from the real source - xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # build a forwarded NPDU to send out xpdu = ForwardedNPDU(pdu.bvlciAddress, pdu, destination=None, user_data=pdu.pduUserData) if _debug: BIPBBMD._debug(" - forwarded xpdu: %r", xpdu) - # look for self as first entry in the BDT - if self.bbmdBDT and (self.bbmdBDT[0] == self.bbmdAddress): - xpdu.pduDestination = LocalBroadcast() - if _debug: BIPBBMD._debug(" - local broadcast") - self.request(xpdu) + # if this was unicast to us, do next hop + if pdu.pduDestination.addrType == Address.localStationAddr: + if _debug: BIPBBMD._debug(" - unicast message") + + # if this BBMD is listed in its BDT, send a local broadcast + if self.bbmdAddress in self.bbmdBDT: + xpdu.pduDestination = LocalBroadcast() + if _debug: BIPBBMD._debug(" - local broadcast") + self.request(xpdu) + + elif pdu.pduDestination.addrType == Address.localBroadcastAddr: + if _debug: BIPBBMD._debug(" - directed broadcast message") + + else: + BIPBBMD._warning("invalid destination address: %r", pdu.pduDestination) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) elif isinstance(pdu, RegisterForeignDevice): @@ -820,12 +840,13 @@ def confirmation(self, pdu): self.request(xpdu) elif isinstance(pdu, DistributeBroadcastToNetwork): - # build a PDU with a local broadcast address - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # build a forwarded NPDU to send out xpdu = ForwardedNPDU(pdu.pduSource, pdu, user_data=pdu.pduUserData) @@ -835,35 +856,37 @@ def confirmation(self, pdu): for bdte in self.bbmdBDT: if bdte == self.bbmdAddress: xpdu.pduDestination = LocalBroadcast() - if _debug: BIPBBMD._debug(" - local broadcast") + if _debug: BIPBBMD._debug(" - local broadcast") self.request(xpdu) else: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the other registered foreign devices for fdte in self.bbmdFDT: if fdte.fdAddress != pdu.pduSource: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) elif isinstance(pdu, OriginalUnicastNPDU): - # build a vanilla PDU - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) elif isinstance(pdu, OriginalBroadcastNPDU): - # build a PDU with a local broadcast address - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # make a forwarded PDU xpdu = ForwardedNPDU(pdu.pduSource, pdu, user_data=pdu.pduUserData) @@ -873,13 +896,13 @@ def confirmation(self, pdu): for bdte in self.bbmdBDT: if bdte != self.bbmdAddress: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) else: @@ -929,7 +952,7 @@ def delete_foreign_device_table_entry(self, addr): del self.bbmdFDT[i] break else: - stat = 99 ### entry not found + stat = 0x0050 ### entry not found # return status return stat @@ -956,10 +979,6 @@ def add_peer(self, addr): else: raise TypeError("addr must be a string or an Address") - # if it's this BBMD, make it the first one - if self.bbmdBDT and (addr == self.bbmdAddress): - raise RuntimeError("add self to BDT as first address") - # see if it's already there for bdte in self.bbmdBDT: if addr == bdte: diff --git a/py25/bacpypes/local/device.py b/py25/bacpypes/local/device.py index 7af52a7d..bab2bdd3 100644 --- a/py25/bacpypes/local/device.py +++ b/py25/bacpypes/local/device.py @@ -2,7 +2,9 @@ from ..debugging import bacpypes_debugging, ModuleLogger -from ..primitivedata import Date, Time, ObjectIdentifier +from ..primitivedata import Null, Boolean, Unsigned, Integer, Real, Double, \ + OctetString, CharacterString, BitString, Enumerated, Date, Time, \ + ObjectIdentifier from ..constructeddata import ArrayOf from ..basetypes import ServicesSupported @@ -107,21 +109,113 @@ class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): def __init__(self, **kwargs): if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) - # fill in default property values not in kwargs - for attr, value in LocalDeviceObject.defaultProperties.items(): - if attr not in kwargs: - kwargs[attr] = value - - for key, value in kwargs.items(): - if key.startswith("_"): - setattr(self, key, value) - del kwargs[key] + # start with an empty dictionary of device object properties + init_args = {} + ini_arg = kwargs.get('ini', None) + if _debug: LocalDeviceObject._debug(" - ini_arg: %r", dir(ini_arg)) - # check for registration + # check for registration as a keyword parameter or in the INI file if self.__class__ not in registered_object_types.values(): - if 'vendorIdentifier' not in kwargs: + if _debug: LocalDeviceObject._debug(" - unregistered") + + vendor_identifier = kwargs.get('vendorIdentifier', None) + if _debug: LocalDeviceObject._debug(" - keyword vendor identifier: %r", vendor_identifier) + + if vendor_identifier is None: + vendor_identifier = getattr(ini_arg, 'vendoridentifier', None) + if _debug: LocalDeviceObject._debug(" - INI vendor identifier: %r", vendor_identifier) + + if vendor_identifier is None: raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") - register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) + + register_object_type(self.__class__, vendor_id=vendor_identifier) + + # look for properties, fill in values from the keyword arguments or + # the INI parameter (converted to a proper value) if it was provided + for propid, prop in self._properties.items(): + # special processing for object identifier + if propid == 'objectIdentifier': + continue + + # use keyword argument if it was provided + if propid in kwargs: + prop_value = kwargs[propid] + else: + prop_value = getattr(ini_arg, propid.lower(), None) + if prop_value is None: + continue + + prop_datatype = prop.datatype + + if issubclass(prop_datatype, Null): + if prop_value != "Null": + raise ValueError("invalid null property value: %r" % (propid,)) + prop_value = None + + elif issubclass(prop_datatype, Boolean): + prop_value = prop_value.lower() + if prop_value not in ('true', 'false', 'set', 'reset'): + raise ValueError("invalid boolean property value: %r" % (propid,)) + prop_value = prop_value in ('true', 'set') + + elif issubclass(prop_datatype, (Unsigned, Integer)): + try: + prop_value = int(prop_value) + except ValueError: + raise ValueError("invalid unsigned or integer property value: %r" % (propid,)) + + elif issubclass(prop_datatype, (Real, Double)): + try: + prop_value = float(prop_value) + except ValueError: + raise ValueError("invalid real or double property value: %r" % (propid,)) + + elif issubclass(prop_datatype, OctetString): + try: + prop_value = xtob(prop_value) + except: + raise ValueError("invalid octet string property value: %r" % (propid,)) + + elif issubclass(prop_datatype, CharacterString): + pass + + elif issubclass(prop_datatype, BitString): + try: + bstr, prop_value = prop_value, [] + for b in bstr: + if b not in ('0', '1'): + raise ValueError + prop_value.append(int(b)) + except: + raise ValueError("invalid bit string property value: %r" % (propid,)) + + elif issubclass(prop_datatype, Enumerated): + pass + + else: + raise ValueError("cannot interpret %r INI paramter" % (propid,)) + if _debug: LocalDeviceObject._debug(" - property %r: %r", propid, prop_value) + + # at long last + init_args[propid] = prop_value + + # check for object identifier as a keyword parameter or in the INI file, + # and it might be just an int, so make it a tuple if necessary + if 'objectIdentifier' in kwargs: + object_identifier = kwargs['objectIdentifier'] + if isinstance(object_identifier, (int, long)): + object_identifier = ('device', object_identifier) + elif hasattr(ini_arg, 'objectidentifier'): + object_identifier = ('device', int(getattr(ini_arg, 'objectidentifier'))) + else: + raise RuntimeError("objectIdentifier is required") + init_args['objectIdentifier'] = object_identifier + if _debug: LocalDeviceObject._debug(" - object identifier: %r", object_identifier) + + # fill in default property values not in init_args + for attr, value in LocalDeviceObject.defaultProperties.items(): + if attr not in init_args: + init_args[attr] = value # check for properties this class implements if 'localDate' in kwargs: @@ -131,29 +225,25 @@ def __init__(self, **kwargs): if 'protocolServicesSupported' in kwargs: raise RuntimeError("protocolServicesSupported is provided by LocalDeviceObject and cannot be overridden") - # the object identifier is required for the object list - if 'objectIdentifier' not in kwargs: - raise RuntimeError("objectIdentifier is required") - - # coerce the object identifier - object_identifier = kwargs['objectIdentifier'] - if isinstance(object_identifier, (int, long)): - object_identifier = ('device', object_identifier) - # the object list is provided if 'objectList' in kwargs: raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") - kwargs['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) + init_args['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) # check for a minimum value - if kwargs['maxApduLengthAccepted'] < 50: + if init_args['maxApduLengthAccepted'] < 50: raise ValueError("invalid max APDU length accepted") # dump the updated attributes - if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) + if _debug: LocalDeviceObject._debug(" - init_args: %r", init_args) # proceed as usual - super(LocalDeviceObject, self).__init__(**kwargs) + super(LocalDeviceObject, self).__init__(**init_args) + + # pass along special property values that are not BACnet properties + for key, value in kwargs.items(): + if key.startswith("_"): + setattr(self, key, value) bacpypes_debugging(LocalDeviceObject) diff --git a/py25/bacpypes/object.py b/py25/bacpypes/object.py index 12b8d819..c5ce0cae 100755 --- a/py25/bacpypes/object.py +++ b/py25/bacpypes/object.py @@ -1440,7 +1440,7 @@ class EventEnrollmentObject(Object): , ReadableProperty('eventTimeStamps', ArrayOf(TimeStamp)) , OptionalProperty('eventMessageTexts', ArrayOf(CharacterString)) , OptionalProperty('eventMessageTextsConfig', ArrayOf(CharacterString)) - , OptionalProperty('eventDetectionEnable', Boolean) + , ReadableProperty('eventDetectionEnable', Boolean) , OptionalProperty('eventAlgorithmInhibitRef', ObjectPropertyReference) , OptionalProperty('eventAlgorithmInhibit', Boolean) , OptionalProperty('timeDelayNormal', Unsigned) diff --git a/py25/bacpypes/task.py b/py25/bacpypes/task.py index a761da4c..04096bbb 100755 --- a/py25/bacpypes/task.py +++ b/py25/bacpypes/task.py @@ -8,6 +8,7 @@ from time import time as _time from heapq import heapify, heappush, heappop +import itertools from .singleton import SingletonLogging from .debugging import DebugContents, Logging, ModuleLogger, bacpypes_debugging @@ -280,6 +281,9 @@ def __init__(self): # task manager is this instance _task_manager = self + # unique sequence counter for tasks scheduled at the same time + self.counter = itertools.count() + # there may be tasks created that couldn't be scheduled # because a task manager wasn't created yet. if _unscheduled_tasks: @@ -304,7 +308,7 @@ def install_task(self, task): self.suspend_task(task) # save this in the task list - heappush( self.tasks, (task.taskTime, task) ) + heappush( self.tasks, (task.taskTime, next(self.counter), task) ) if _debug: TaskManager._debug(" - tasks: %r", self.tasks) task.isScheduled = True @@ -317,7 +321,7 @@ def suspend_task(self, task): if _debug: TaskManager._debug("suspend_task %r", task) # remove this guy - for i, (when, curtask) in enumerate(self.tasks): + for i, (when, n, curtask) in enumerate(self.tasks): if task is curtask: if _debug: TaskManager._debug(" - task found") del self.tasks[i] @@ -352,7 +356,7 @@ def get_next_task(self): if self.tasks: # look at the first task - when, nxttask = self.tasks[0] + when, n, nxttask = self.tasks[0] if when <= now: # pull it off the list and mark that it's no longer scheduled heappop(self.tasks) @@ -360,7 +364,7 @@ def get_next_task(self): task.isScheduled = False if self.tasks: - when, nxttask = self.tasks[0] + when, n, nxttask = self.tasks[0] # peek at the next task, return how long to wait delta = max(when - now, 0.0) else: diff --git a/py25/bacpypes/vlan.py b/py25/bacpypes/vlan.py index 4d716964..fef75358 100755 --- a/py25/bacpypes/vlan.py +++ b/py25/bacpypes/vlan.py @@ -161,9 +161,9 @@ class IPNetwork(Network): ('1.2.3.255', 5) and the other nodes must have the same tuple. """ - def __init__(self): + def __init__(self, name=''): if _debug: IPNetwork._debug("__init__") - Network.__init__(self) + Network.__init__(self, name=name) def add_node(self, node): if _debug: IPNetwork._debug("add_node %r", node) @@ -213,11 +213,12 @@ def __init__(self, addr, lan=None, promiscuous=False, spoofing=False, sid=None): class IPRouterNode(Client): - def __init__(self, router, addr, lan=None): + def __init__(self, router, addr, lan): if _debug: IPRouterNode._debug("__init__ %r %r lan=%r", router, addr, lan) - # save the reference to the router + # save the references to the router for packets and the lan for debugging self.router = router + self.lan = lan # make ourselves an IPNode and bind to it self.node = IPNode(addr, lan=lan, promiscuous=True, spoofing=True) @@ -238,6 +239,9 @@ def process_pdu(self, pdu): # pass it downstream self.request(pdu) + def __repr__(self): + return "<%s for %s>" % (self.__class__.__name__, self.lan.name) + bacpypes_debugging(IPRouterNode) # diff --git a/py27/bacpypes/__init__.py b/py27/bacpypes/__init__.py index 70e98038..3adf9c1c 100755 --- a/py27/bacpypes/__init__.py +++ b/py27/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.17.0' +__version__ = '0.17.1' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py27/bacpypes/apdu.py b/py27/bacpypes/apdu.py index 10706aa2..c19a811e 100755 --- a/py27/bacpypes/apdu.py +++ b/py27/bacpypes/apdu.py @@ -56,38 +56,51 @@ def register_error_type(klass): # encode_max_segments_accepted/decode_max_segments_accepted # +_max_segments_accepted_encoding = [ + None, 2, 4, 8, 16, 32, 64, None, + ] + def encode_max_segments_accepted(arg): """Encode the maximum number of segments the device will accept, Section - 20.1.2.4""" - w = 0 - while (arg and not arg & 1): - w += 1 - arg = (arg >> 1) - return w + 20.1.2.4, and if the device says it can only accept one segment it shouldn't + say that it supports segmentation!""" + # unspecified + if not arg: + return 0 + + if arg > 64: + return 7 + + # the largest number not greater than the arg + for i in range(6, 0, -1): + if _max_segments_accepted_encoding[i] <= arg: + return i + + raise ValueError("invalid max max segments accepted: {0}".format(arg)) def decode_max_segments_accepted(arg): """Decode the maximum number of segments the device will accept, Section 20.1.2.4""" - return arg and (1 << arg) or None + return _max_segments_accepted_encoding[arg] # # encode_max_apdu_length_accepted/decode_max_apdu_length_accepted # -_max_apdu_response_encoding = [50, 128, 206, 480, 1024, 1476, None, None, +_max_apdu_length_encoding = [50, 128, 206, 480, 1024, 1476, None, None, None, None, None, None, None, None, None, None] def encode_max_apdu_length_accepted(arg): """Return the encoding of the highest encodable value less than the value of the arg.""" for i in range(5, -1, -1): - if (arg >= _max_apdu_response_encoding[i]): + if (arg >= _max_apdu_length_encoding[i]): return i raise ValueError("invalid max APDU length accepted: {0}".format(arg)) def decode_max_apdu_length_accepted(arg): - v = _max_apdu_response_encoding[arg] + v = _max_apdu_length_encoding[arg] if not v: raise ValueError("invalid max APDU length accepted: {0}".format(arg)) @@ -174,7 +187,7 @@ def encode(self, pdu): if self.apduSA: buff += 0x02 pdu.put(buff) - pdu.put((encode_max_segments_accepted(self.apduMaxSegs) << 4) + encode_max_apdu_length_accepted(self.apduMaxResp)) + pdu.put((self.apduMaxSegs << 4) + self.apduMaxResp) pdu.put(self.apduInvokeID) if self.apduSeg: pdu.put(self.apduSeq) @@ -255,8 +268,8 @@ def decode(self, pdu): self.apduMor = ((buff & 0x04) != 0) self.apduSA = ((buff & 0x02) != 0) buff = pdu.get() - self.apduMaxSegs = decode_max_segments_accepted( (buff >> 4) & 0x07 ) - self.apduMaxResp = decode_max_apdu_length_accepted( buff & 0x0F ) + self.apduMaxSegs = (buff >> 4) & 0x07 + self.apduMaxResp = buff & 0x0F self.apduInvokeID = pdu.get() if self.apduSeg: self.apduSeq = pdu.get() @@ -1301,7 +1314,7 @@ class LifeSafetyOperationRequest(ConfirmedRequestSequence): [ Element('requestingProcessIdentifier', Unsigned, 0) , Element('requestingSource', CharacterString, 1) , Element('request', LifeSafetyOperation, 2) - , Element('objectIdentifier', ObjectIdentifier, 3) + , Element('objectIdentifier', ObjectIdentifier, 3, True) ] register_confirmed_request_type(LifeSafetyOperationRequest) @@ -1485,7 +1498,7 @@ class RemoveListElementRequest(ConfirmedRequestSequence): sequenceElements = \ [ Element('objectIdentifier', ObjectIdentifier, 0) , Element('propertyIdentifier', PropertyIdentifier, 1) - , Element('propertyArrayIndex', Unsigned, 2) + , Element('propertyArrayIndex', Unsigned, 2, True) , Element('listOfElements', Any, 3) ] diff --git a/py27/bacpypes/app.py b/py27/bacpypes/app.py index a1081497..1548a3b4 100755 --- a/py27/bacpypes/app.py +++ b/py27/bacpypes/app.py @@ -26,7 +26,8 @@ # for computing protocol services supported from .apdu import confirmed_request_types, unconfirmed_request_types, \ - ConfirmedServiceChoice, UnconfirmedServiceChoice + ConfirmedServiceChoice, UnconfirmedServiceChoice, \ + IAmRequest from .basetypes import ServicesSupported # basic services @@ -54,16 +55,16 @@ class DeviceInfo(DebugContents): 'maxSegmentsAccepted', ) - def __init__(self): + def __init__(self, device_identifier, address): # this information is from an IAmRequest - self.deviceIdentifier = None # device identifier - self.address = None # LocalStation or RemoteStation + self.deviceIdentifier = device_identifier + self.address = address + self.maxApduLengthAccepted = 1024 # maximum APDU device will accept self.segmentationSupported = 'noSegmentation' # normally no segmentation + self.maxSegmentsAccepted = None # None iff no segmentation self.vendorID = None # vendor identifier - - self.maxNpduLength = 1497 # maximum we can send in transit - self.maxSegmentsAccepted = None # value for proposed/actual window size + self.maxNpduLength = None # maximum we can send in transit (see 19.4) # # DeviceInfoCache @@ -72,127 +73,134 @@ def __init__(self): @bacpypes_debugging class DeviceInfoCache: - def __init__(self): + def __init__(self, device_info_class=DeviceInfo): if _debug: DeviceInfoCache._debug("__init__") + # a little error checking + if not issubclass(device_info_class, DeviceInfo): + raise ValueError("not a DeviceInfo subclass: %r" % (device_info_class,)) + # empty cache self.cache = {} + # class for new records + self.device_info_class = device_info_class + def has_device_info(self, key): """Return true iff cache has information about the device.""" if _debug: DeviceInfoCache._debug("has_device_info %r", key) return key in self.cache - def add_device_info(self, apdu): + def iam_device_info(self, apdu): """Create a device information record based on the contents of an IAmRequest and put it in the cache.""" - if _debug: DeviceInfoCache._debug("add_device_info %r", apdu) + if _debug: DeviceInfoCache._debug("iam_device_info %r", apdu) - # get the existing cache record by identifier - info = self.get_device_info(apdu.iAmDeviceIdentifier[1]) - if _debug: DeviceInfoCache._debug(" - info: %r", info) + # make sure the apdu is an I-Am + if not isinstance(apdu, IAmRequest): + raise ValueError("not an IAmRequest: %r" % (apdu,)) - # update existing record - if info: - if (info.address == apdu.pduSource): - return + # get the device instance + device_instance = apdu.iAmDeviceIdentifier[1] - info.address = apdu.pduSource - else: - # get the existing record by address (creates a new record) - info = self.get_device_info(apdu.pduSource) - if _debug: DeviceInfoCache._debug(" - info: %r", info) + # get the existing cache record if it exists + device_info = self.cache.get(device_instance, None) - info.deviceIdentifier = apdu.iAmDeviceIdentifier[1] + # maybe there is a record for this address + if not device_info: + device_info = self.cache.get(apdu.pduSource, None) - # update the rest of the values - info.maxApduLengthAccepted = apdu.maxAPDULengthAccepted - info.segmentationSupported = apdu.segmentationSupported - info.vendorID = apdu.vendorID + # make a new one using the class provided + if not device_info: + device_info = self.device_info_class(device_instance, apdu.pduSource) - # say this is an updated record - self.update_device_info(info) + # jam in the correct values + device_info.deviceIdentifier = device_instance + device_info.address = apdu.pduSource + device_info.maxApduLengthAccepted = apdu.maxAPDULengthAccepted + device_info.segmentationSupported = apdu.segmentationSupported + device_info.vendorID = apdu.vendorID + + # tell the cache this is an updated record + self.update_device_info(device_info) def get_device_info(self, key): - """Return the known information about the device. If the key is the - address of an unknown device, build a generic device information record - add put it in the cache.""" if _debug: DeviceInfoCache._debug("get_device_info %r", key) - if isinstance(key, int): - current_info = self.cache.get(key, None) + # get the info if it's there + device_info = self.cache.get(key, None) + if _debug: DeviceInfoCache._debug(" - device_info: %r", device_info) - elif not isinstance(key, Address): - raise TypeError("key must be integer or an address") + return device_info - elif key.addrType not in (Address.localStationAddr, Address.remoteStationAddr): - raise TypeError("address must be a local or remote station") - - else: - current_info = self.cache.get(key, None) - if not current_info: - current_info = DeviceInfo() - current_info.address = key - current_info._cache_keys = (None, key) - current_info._ref_count = 1 - - self.cache[key] = current_info - else: - if _debug: DeviceInfoCache._debug(" - reference bump") - current_info._ref_count += 1 - - if _debug: DeviceInfoCache._debug(" - current_info: %r", current_info) - - return current_info - - def update_device_info(self, info): + def update_device_info(self, device_info): """The application has updated one or more fields in the device information record and the cache needs to be updated to reflect the changes. If this is a cached version of a persistent record then this is the opportunity to update the database.""" - if _debug: DeviceInfoCache._debug("update_device_info %r", info) + if _debug: DeviceInfoCache._debug("update_device_info %r", device_info) - cache_id, cache_address = info._cache_keys + # give this a reference count if it doesn't have one + if not hasattr(device_info, '_ref_count'): + device_info._ref_count = 0 - if (cache_id is not None) and (info.deviceIdentifier != cache_id): + # get the current keys + cache_id, cache_address = getattr(device_info, '_cache_keys', (None, None)) + + if (cache_id is not None) and (device_info.deviceIdentifier != cache_id): if _debug: DeviceInfoCache._debug(" - device identifier updated") # remove the old reference, add the new one del self.cache[cache_id] - self.cache[info.deviceIdentifier] = info - - cache_id = info.deviceIdentifier + self.cache[device_info.deviceIdentifier] = device_info - if (cache_address is not None) and (info.address != cache_address): + if (cache_address is not None) and (device_info.address != cache_address): if _debug: DeviceInfoCache._debug(" - device address updated") # remove the old reference, add the new one del self.cache[cache_address] - self.cache[info.address] = info - - cache_address = info.address + self.cache[device_info.address] = device_info # update the keys - info._cache_keys = (cache_id, cache_address) + device_info._cache_keys = (device_info.deviceIdentifier, device_info.address) + + def acquire(self, key): + """Return the known information about the device and mark the record + as being used by a segmenation state machine.""" + if _debug: DeviceInfoCache._debug("acquire %r", key) + + if isinstance(key, int): + device_info = self.cache.get(key, None) + + elif not isinstance(key, Address): + raise TypeError("key must be integer or an address") + + elif key.addrType not in (Address.localStationAddr, Address.remoteStationAddr): + raise TypeError("address must be a local or remote station") + + else: + device_info = self.cache.get(key, None) + + if device_info: + if _debug: DeviceInfoCache._debug(" - reference bump") + device_info._ref_count += 1 + + if _debug: DeviceInfoCache._debug(" - device_info: %r", device_info) - def release_device_info(self, info): + return device_info + + def release(self, device_info): """This function is called by the segmentation state machine when it has finished with the device information.""" - if _debug: DeviceInfoCache._debug("release_device_info %r", info) + if _debug: DeviceInfoCache._debug("release %r", device_info) # this information record might be used by more than one SSM - if info._ref_count > 1: - if _debug: DeviceInfoCache._debug(" - multiple references") - info._ref_count -= 1 - return + if device_info._ref_count == 0: + raise RuntimeError("reference count") - cache_id, cache_address = info._cache_keys - if cache_id is not None: - del self.cache[cache_id] - if cache_address is not None: - del self.cache[cache_address] - if _debug: DeviceInfoCache._debug(" - released") + # decrement the reference count + device_info._ref_count -= 1 # # Application @@ -260,9 +268,9 @@ def add_object(self, obj): # make sure it hasn't already been defined if object_name in self.objectName: - raise RuntimeError("already an object with name {0!r}".format(object_name)) + raise RuntimeError("already an object with name %r" % (object_name,)) if object_identifier in self.objectIdentifier: - raise RuntimeError("already an object with identifier {0!r}".format(object_identifier)) + raise RuntimeError("already an object with identifier %r" % (object_identifier,)) # now put it in local dictionaries self.objectName[object_name] = obj diff --git a/py27/bacpypes/appservice.py b/py27/bacpypes/appservice.py index 4a3b65da..641e6554 100755 --- a/py27/bacpypes/appservice.py +++ b/py27/bacpypes/appservice.py @@ -12,12 +12,14 @@ from .task import OneShotTask from .pdu import Address -from .apdu import AbortPDU, AbortReason, ComplexAckPDU, \ +from .apdu import encode_max_segments_accepted, decode_max_segments_accepted, \ + encode_max_apdu_length_accepted, decode_max_apdu_length_accepted, \ + 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 +from .errors import RejectException, AbortException, UnrecognizedService # some debugging _debug = 0 @@ -45,19 +47,23 @@ class SSM(OneShotTask, DebugContents): , 'SEGMENTED_RESPONSE', 'SEGMENTED_CONFIRMATION', 'COMPLETED', 'ABORTED' ] - _debug_contents = ('ssmSAP', 'localDevice', 'remoteDevice', 'invokeID' + _debug_contents = ('ssmSAP', 'localDevice', 'device_info', 'invokeID' , 'state', 'segmentAPDU', 'segmentSize', 'segmentCount', 'maxSegmentsAccepted' , 'retryCount', 'segmentRetryCount', 'sentAllSegments', 'lastSequenceNumber' , 'initialSequenceNumber', 'actualWindowSize', 'proposedWindowSize' ) - def __init__(self, sap, remoteDevice): + def __init__(self, sap, pdu_address): """Common parts for client and server segmentation.""" - if _debug: SSM._debug("__init__ %r %r", sap, remoteDevice) + if _debug: SSM._debug("__init__ %r %r", sap, pdu_address) OneShotTask.__init__(self) self.ssmSAP = sap # service access point - self.remoteDevice = remoteDevice # remote device information, a DeviceInfo instance + + # save the address and get the device information + self.pdu_address = pdu_address + self.device_info = sap.deviceInfoCache.get_device_info(pdu_address) + self.invokeID = None # invoke ID self.state = IDLE # initial state @@ -71,11 +77,17 @@ def __init__(self, sap, remoteDevice): 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 + # local device object provides these or SAP provides defaults, make + # copies here so they are consistent throughout the transaction but + # they could change from one transaction to the next + self.numberOfApduRetries = getattr(sap.localDevice, 'numberOfApduRetries', sap.numberOfApduRetries) + self.apduTimeout = getattr(sap.localDevice, 'apduTimeout', sap.apduTimeout) + + self.segmentationSupported = getattr(sap.localDevice, 'segmentationSupported', sap.segmentationSupported) + self.segmentTimeout = getattr(sap.localDevice, 'segmentTimeout', sap.segmentTimeout) + self.maxSegmentsAccepted = getattr(sap.localDevice, 'maxSegmentsAccepted', sap.maxSegmentsAccepted) + self.maxApduLengthAccepted = getattr(sap.localDevice, 'maxApduLengthAccepted', sap.maxApduLengthAccepted) def start_timer(self, msecs): if _debug: SSM._debug("start_timer %r", msecs) @@ -146,19 +158,19 @@ def get_segment(self, indx): # check for invalid segment number if indx >= self.segmentCount: - raise RuntimeError("invalid segment number {0}, APDU has {1} segments".format(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; + segAPDU.apduMaxSegs = encode_max_segments_accepted(self.maxSegmentsAccepted) + segAPDU.apduMaxResp = encode_max_apdu_length_accepted(self.maxApduLengthAccepted) + segAPDU.apduInvokeID = self.invokeID # segmented response accepted? - segAPDU.apduSA = self.ssmSAP.segmentationSupported in ('segmentedReceive', 'segmentedBoth') + segAPDU.apduSA = self.segmentationSupported in ('segmentedReceive', 'segmentedBoth') if _debug: SSM._debug(" - segmented response accepted: %r", segAPDU.apduSA) elif self.segmentAPDU.apduType == ComplexAckPDU.pduType: @@ -172,14 +184,21 @@ def get_segment(self, indx): segAPDU.pduUserData = self.segmentAPDU.pduUserData # make sure the destination is set - segAPDU.pduDestination = self.remoteDevice.address + segAPDU.pduDestination = self.pdu_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 + + # first segment sends proposed window size, rest get actual + if indx == 0: + if _debug: SSM._debug(" - proposedWindowSize: %r", self.proposedWindowSize) + segAPDU.apduWin = self.proposedWindowSize + else: + if _debug: SSM._debug(" - actualWindowSize: %r", self.actualWindowSize) + segAPDU.apduWin = self.actualWindowSize else: segAPDU.apduSeg = False segAPDU.apduMor = False @@ -211,10 +230,11 @@ def in_window(self, seqA, seqB): return rslt - def FillWindow(self, seqNum): + def fill_window(self, seqNum): """This function sends all of the packets necessary to fill out the segmentation window.""" - if _debug: SSM._debug("FillWindow %r", seqNum) + if _debug: SSM._debug("fill_window %r", seqNum) + if _debug: SSM._debug(" - actualWindowSize: %r", self.actualWindowSize) for ix in range(self.actualWindowSize): apdu = self.get_segment(seqNum + ix) @@ -234,13 +254,18 @@ def FillWindow(self, seqNum): @bacpypes_debugging class ClientSSM(SSM): - def __init__(self, sap, remoteDevice): - if _debug: ClientSSM._debug("__init__ %s %r", sap, remoteDevice) - SSM.__init__(self, sap, remoteDevice) + def __init__(self, sap, pdu_address): + if _debug: ClientSSM._debug("__init__ %s %r", sap, pdu_address) + SSM.__init__(self, sap, pdu_address) # initialize the retry count self.retryCount = 0 + # acquire the device info + if self.device_info: + if _debug: ClientSSM._debug(" - acquire device information") + self.ssmSAP.deviceInfoCache.acquire(self.device_info) + 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) @@ -253,8 +278,10 @@ def set_state(self, newState, timer=0): 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) + # release the device info + if self.device_info: + if _debug: ClientSSM._debug(" - release device information") + self.ssmSAP.deviceInfoCache.release(self.device_info) def request(self, apdu): """This function is called by client transaction functions when it wants @@ -263,7 +290,7 @@ def request(self, apdu): # make sure it has a good source and destination apdu.pduSource = None - apdu.pduDestination = self.remoteDevice.address + apdu.pduDestination = self.pdu_address # send it via the device self.ssmSAP.request(apdu) @@ -280,26 +307,27 @@ def indication(self, apdu): # 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) + # if the max apdu length of the server isn't known, assume that it + # is the same size as our own and will be the segment size + if (not self.device_info) or (self.device_info.maxApduLengthAccepted is None): + self.segmentSize = self.maxApduLengthAccepted - # the maximum number of segments acceptable in the reply - if apdu.apduMaxSegs is not None: - # this request overrides the default - self.maxSegmentsAccepted = apdu.apduMaxSegs + # if the max npdu length of the server isn't known, assume that it + # is the same as the max apdu length accepted + elif self.device_info.maxNpduLength is None: + self.segmentSize = self.device_info.maxApduLengthAccepted + + # the segment size is the minimum of the size of the largest packet + # that can be delivered to the server and the largest it can accept + else: + self.segmentSize = min(self.device_info.maxNpduLength, self.device_info.maxApduLengthAccepted) + if _debug: ClientSSM._debug(" - segment size: %r", self.segmentSize) # save the invoke ID self.invokeID = apdu.apduInvokeID if _debug: ClientSSM._debug(" - invoke ID: %r", self.invokeID) - # compute the segment count ### minus the header? + # compute the segment count if not apdu.pduData: # always at least one segment self.segmentCount = 1 @@ -312,34 +340,49 @@ def indication(self, apdu): # make sure we support segmented transmit if we need to if self.segmentCount > 1: - if self.ssmSAP.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): + if self.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") + + if not self.device_info: + if _debug: ClientSSM._debug(" - no server info for segmentation support") + + elif self.device_info.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if _debug: ClientSSM._debug(" - server can't receive segmented requests") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return - ### check for APDUTooLong? + # make sure we dont exceed the number of segments in our request + # that the server said it was willing to accept + if not self.device_info: + if _debug: ClientSSM._debug(" - no server info for maximum number of segments") + + elif not self.device_info.maxSegmentsAccepted: + if _debug: ClientSSM._debug(" - server doesn't say maximum number of segments") + + elif self.segmentCount > self.device_info.maxSegmentsAccepted: + if _debug: ClientSSM._debug(" - server can't receive enough segments") + abort = self.abort(AbortReason.apduTooLong) + self.response(abort) + return # send out the first segment (or the whole thing) if self.segmentCount == 1: - # SendConfirmedUnsegmented + # unsegmented self.sentAllSegments = True self.retryCount = 0 - self.set_state(AWAIT_CONFIRMATION, self.ssmSAP.retryTimeout) + self.set_state(AWAIT_CONFIRMATION, self.apduTimeout) else: - # SendConfirmedSegmented + # segmented 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) + self.actualWindowSize = None # segment ack will set value + self.set_state(SEGMENTED_REQUEST, self.segmentTimeout) # deliver to the device self.request(self.get_segment(0)) @@ -350,7 +393,7 @@ def response(self, apdu): if _debug: ClientSSM._debug("response %r", apdu) # make sure it has a good source and destination - apdu.pduSource = self.remoteDevice.address + apdu.pduSource = self.pdu_address apdu.pduDestination = None # send it to the application @@ -407,29 +450,31 @@ def segmented_request(self, apdu): and receives an apdu.""" if _debug: ClientSSM._debug("segmented_request %r", apdu) - # client is ready for the next segment + # server is ready for the next segment if apdu.apduType == SegmentAckPDU.pduType: if _debug: ClientSSM._debug(" - segment ack") + # actual window size is provided by server + self.actualWindowSize = apdu.apduWin + # 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) + self.restart_timer(self.segmentTimeout) # final ack received? elif self.sentAllSegments: if _debug: ClientSSM._debug(" - all done sending request") - self.set_state(AWAIT_CONFIRMATION, self.ssmSAP.retryTimeout) + self.set_state(AWAIT_CONFIRMATION, self.apduTimeout) # 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) + self.fill_window(self.initialSequenceNumber) + self.restart_timer(self.segmentTimeout) # simple ack elif (apdu.apduType == SimpleAckPDU.pduType): @@ -452,6 +497,7 @@ def segmented_request(self, apdu): self.response(abort) # send it to the application elif not apdu.apduSeg: + # ack is not segmented self.set_state(COMPLETED) self.response(apdu) @@ -459,10 +505,11 @@ def segmented_request(self, apdu): # set the segmented response context self.set_segmentation_context(apdu) - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + # minimum of what the server is proposing and this client proposes + self.actualWindowSize = min(apdu.apduWin, self.proposedWindowSize) self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_CONFIRMATION, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_CONFIRMATION, self.segmentTimeout) # some kind of problem elif (apdu.apduType == ErrorPDU.pduType) or (apdu.apduType == RejectPDU.pduType) or (apdu.apduType == AbortPDU.pduType): @@ -479,12 +526,12 @@ def segmented_request_timeout(self): if _debug: ClientSSM._debug("segmented_request_timeout") # try again - if self.segmentRetryCount < self.ssmSAP.retryCount: + if self.segmentRetryCount < self.numberOfApduRetries: if _debug: ClientSSM._debug(" - retry segmented request") self.segmentRetryCount += 1 - self.start_timer(self.ssmSAP.segmentTimeout) - self.FillWindow(self.initialSequenceNumber) + self.start_timer(self.segmentTimeout) + self.fill_window(self.initialSequenceNumber) else: if _debug: ClientSSM._debug(" - abort, no response from the device") @@ -516,7 +563,7 @@ def await_confirmation(self, apdu): self.set_state(COMPLETED) self.response(apdu) - elif self.ssmSAP.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + elif self.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): if _debug: ClientSSM._debug(" - local device can't receive segmented messages") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) @@ -527,10 +574,10 @@ def await_confirmation(self, apdu): # set the segmented response context self.set_segmentation_context(apdu) - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + self.actualWindowSize = apdu.apduWin self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_CONFIRMATION, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_CONFIRMATION, self.segmentTimeout) # send back a segment ack segack = SegmentAckPDU( 0, 0, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) @@ -546,7 +593,7 @@ def await_confirmation(self, apdu): elif (apdu.apduType == SegmentAckPDU.pduType): if _debug: ClientSSM._debug(" - segment ack(!?)") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) else: raise RuntimeError("invalid APDU (3)") @@ -554,9 +601,9 @@ def await_confirmation(self, apdu): 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) + if self.retryCount < self.numberOfApduRetries: + if _debug: ClientSSM._debug(" - no response, try again (%d < %d)", self.retryCount, self.numberOfApduRetries) + self.retryCount += 1 # save the retry count, indication acts like the request is coming # from the application so the retryCount gets re-initialized. @@ -594,8 +641,8 @@ def segmented_confirmation(self, apdu): 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.restart_timer(self.segmentTimeout) + segack = SegmentAckPDU(1, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) return @@ -610,7 +657,7 @@ def segmented_confirmation(self, apdu): if _debug: ClientSSM._debug(" - no more follows") # send a final ack - segack = SegmentAckPDU( 0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) self.set_state(COMPLETED) @@ -620,15 +667,15 @@ def segmented_confirmation(self, apdu): 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.restart_timer(self.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) + self.restart_timer(self.segmentTimeout) def segmented_confirmation_timeout(self): if _debug: ClientSSM._debug("segmented_confirmation_timeout") @@ -643,9 +690,14 @@ def segmented_confirmation_timeout(self): @bacpypes_debugging class ServerSSM(SSM): - def __init__(self, sap, remoteDevice): - if _debug: ServerSSM._debug("__init__ %s %r", sap, remoteDevice) - SSM.__init__(self, sap, remoteDevice) + def __init__(self, sap, pdu_address): + if _debug: ServerSSM._debug("__init__ %s %r", sap, pdu_address) + SSM.__init__(self, sap, pdu_address) + + # acquire the device info + if self.device_info: + if _debug: ServerSSM._debug(" - acquire device information") + self.ssmSAP.deviceInfoCache.acquire(self.device_info) def set_state(self, newState, timer=0): """This function is called when the client wants to change state.""" @@ -659,8 +711,10 @@ def set_state(self, newState, timer=0): 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) + # release the device info + if self.device_info: + if _debug: ClientSSM._debug(" - release device information") + self.ssmSAP.deviceInfoCache.release(self.device_info) def request(self, apdu): """This function is called by transaction functions to send @@ -668,7 +722,7 @@ def request(self, apdu): if _debug: ServerSSM._debug("request %r", apdu) # make sure it has a good source and destination - apdu.pduSource = self.remoteDevice.address + apdu.pduSource = self.pdu_address apdu.pduDestination = None # send it via the device @@ -697,7 +751,7 @@ def response(self, apdu): # make sure it has a good source and destination apdu.pduSource = None - apdu.pduDestination = self.remoteDevice.address + apdu.pduDestination = self.pdu_address # send it via the device self.ssmSAP.request(apdu) @@ -739,14 +793,15 @@ def confirmation(self, apdu): # 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) + # the segment size is the minimum of the size of the largest packet + # that can be delivered to the client and the largest it can accept + if (not self.device_info) or (self.device_info.maxNpduLength is None): + self.segmentSize = self.maxApduLengthAccepted + else: + self.segmentSize = min(self.device_info.maxNpduLength, self.maxApduLengthAccepted) if _debug: ServerSSM._debug(" - segment size: %r", self.segmentSize) - # compute the segment count ### minus the header? + # compute the segment count if not apdu.pduData: # always at least one segment self.segmentCount = 1 @@ -762,26 +817,31 @@ def confirmation(self, apdu): 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 self.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): if _debug: ServerSSM._debug(" - server can't send segmented responses") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return # make sure client supports segmented receive - if self.remoteDevice.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if self.device_info.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? + # make sure we dont exceed the number of segments in our response + # that the device said it was willing to accept in the request + if self.segmentCount > self.maxSegmentsAccepted: + if _debug: ServerSSM._debug(" - client can't receive enough segments") + abort = self.abort(AbortReason.apduTooLong) + self.response(abort) + return # initialize the state self.segmentRetryCount = 0 self.initialSequenceNumber = 0 - self.proposedWindowSize = self.ssmSAP.maxSegmentsAccepted - self.actualWindowSize = 1 + self.actualWindowSize = None # send out the first segment (or the whole thing) if self.segmentCount == 1: @@ -789,7 +849,7 @@ def confirmation(self, apdu): self.set_state(COMPLETED) else: self.response(self.get_segment(0)) - self.set_state(SEGMENTED_RESPONSE, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_RESPONSE, self.segmentTimeout) else: raise RuntimeError("invalid APDU (4)") @@ -837,38 +897,48 @@ def idle(self, apdu): 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 not self.device_info: + if _debug: ServerSSM._debug(" - no client device info") + + elif self.device_info.segmentationSupported == 'noSegmentation': if _debug: ServerSSM._debug(" - client actually supports segmented receive") - self.remoteDevice.segmentationSupported = 'segmentedReceive' + self.device_info.segmentationSupported = 'segmentedReceive' if _debug: ServerSSM._debug(" - tell the cache the info has been updated") - self.ssmSAP.deviceInfoCache.update_device_info(self.remoteDevice) + self.ssmSAP.deviceInfoCache.update_device_info(self.device_info) - elif self.remoteDevice.segmentationSupported == 'segmentedTransmit': + elif self.device_info.segmentationSupported == 'segmentedTransmit': if _debug: ServerSSM._debug(" - client actually supports both segmented transmit and receive") - self.remoteDevice.segmentationSupported = 'segmentedBoth' + self.device_info.segmentationSupported = 'segmentedBoth' if _debug: ServerSSM._debug(" - tell the cache the info has been updated") - self.ssmSAP.deviceInfoCache.update_device_info(self.remoteDevice) + self.ssmSAP.deviceInfoCache.update_device_info(self.device_info) - elif self.remoteDevice.segmentationSupported == 'segmentedReceive': + elif self.device_info.segmentationSupported == 'segmentedReceive': pass - elif self.remoteDevice.segmentationSupported == 'segmentedBoth': + elif self.device_info.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?") + # decode the maximum that the client can receive in one APDU, and if + # there is a value in the device information then use that one because + # it came from reading device object property value or from an I-Am + # message that was received + self.maxApduLengthAccepted = decode_max_apdu_length_accepted(apdu.apduMaxResp) + if self.device_info and self.device_info.maxApduLengthAccepted is not None: + if self.device_info.maxApduLengthAccepted < self.maxApduLengthAccepted: + if _debug: ServerSSM._debug(" - apduMaxResp encoding error") + else: + self.maxApduLengthAccepted = self.device_info.maxApduLengthAccepted + if _debug: ServerSSM._debug(" - maxApduLengthAccepted: %r", self.maxApduLengthAccepted) - # save the number of segments the client is willing to accept in the ack - self.maxSegmentsAccepted = apdu.apduMaxSegs + # save the number of segments the client is willing to accept in the ack, + # if this is None then the value is unknown or more than 64 + self.maxSegmentsAccepted = decode_max_segments_accepted(apdu.apduMaxSegs) # unsegmented request if not apdu.apduSeg: @@ -877,7 +947,7 @@ def idle(self, apdu): return # make sure we support segmented requests - if self.ssmSAP.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if self.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return @@ -885,17 +955,18 @@ def idle(self, apdu): # 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) + # the window size is the minimum of what I would propose and what the + # device has proposed + self.actualWindowSize = min(apdu.apduWin, self.proposedWindowSize) + if _debug: ServerSSM._debug(" - actualWindowSize? min(%r, %r) -> %r", apdu.apduWin, self.proposedWindowSize, self.actualWindowSize) # initialize the state self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_REQUEST, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_REQUEST, self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) if _debug: ServerSSM._debug(" - segAck: %r", segack) self.response(segack) @@ -928,10 +999,10 @@ def segmented_request(self, apdu): 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) + self.restart_timer(self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 1, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(1, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) self.response(segack) return @@ -947,7 +1018,7 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - no more follows") # send back a final segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.response(segack) # forward the whole thing to the application @@ -958,17 +1029,17 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - last segment in the group") self.initialSequenceNumber = self.lastSequenceNumber - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + 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) + self.restart_timer(self.segmentTimeout) def segmented_request_timeout(self): if _debug: ServerSSM._debug("segmented_request_timeout") @@ -1008,10 +1079,13 @@ def segmented_response(self, apdu): if (apdu.apduType == SegmentAckPDU.pduType): if _debug: ServerSSM._debug(" - segment ack") + # actual window size is provided by client + self.actualWindowSize = apdu.apduWin + # 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) + self.restart_timer(self.segmentTimeout) # final ack received? elif self.sentAllSegments: @@ -1024,8 +1098,8 @@ def segmented_response(self, apdu): self.initialSequenceNumber = (apdu.apduSeq + 1) % 256 self.actualWindowSize = apdu.apduWin self.segmentRetryCount = 0 - self.FillWindow(self.initialSequenceNumber) - self.restart_timer(self.ssmSAP.segmentTimeout) + self.fill_window(self.initialSequenceNumber) + self.restart_timer(self.segmentTimeout) # some kind of problem elif (apdu.apduType == AbortPDU.pduType): @@ -1039,10 +1113,10 @@ def segmented_response_timeout(self): if _debug: ServerSSM._debug("segmented_response_timeout") # try again - if self.segmentRetryCount < self.ssmSAP.retryCount: + if self.segmentRetryCount < self.numberOfApduRetries: self.segmentRetryCount += 1 - self.start_timer(self.ssmSAP.segmentTimeout) - self.FillWindow(self.initialSequenceNumber) + self.start_timer(self.segmentTimeout) + self.fill_window(self.initialSequenceNumber) else: # give up self.set_state(ABORTED) @@ -1062,6 +1136,7 @@ def __init__(self, localDevice=None, deviceInfoCache=None, sap=None, cid=None): ServiceAccessPoint.__init__(self, sap) # save a reference to the device information cache + self.localDevice = localDevice self.deviceInfoCache = deviceInfoCache # client settings @@ -1072,27 +1147,19 @@ def __init__(self, localDevice=None, deviceInfoCache=None, sap=None, cid=None): self.serverTransactions = [] # confirmed request defaults - self.retryCount = 3 - self.retryTimeout = 3000 + self.numberOfApduRetries = 3 + self.apduTimeout = 3000 self.maxApduLengthAccepted = 1024 # segmentation defaults self.segmentationSupported = 'noSegmentation' self.segmentTimeout = 1500 - self.maxSegmentsAccepted = 8 + self.maxSegmentsAccepted = 2 + self.proposedWindowSize = 2 # 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 @@ -1111,7 +1178,7 @@ def get_next_invoke_id(self, addr): raise RuntimeError("no available invoke ID") for tr in self.clientTransactions: - if (invokeID == tr.invokeID) and (addr == tr.remoteDevice.address): + if (invokeID == tr.invokeID) and (addr == tr.pdu_address): break else: break @@ -1152,14 +1219,11 @@ def confirmation(self, pdu): 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): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: - # find the remote device information - remoteDevice = self.deviceInfoCache.get_device_info(apdu.pduSource) - # build a server transaction - tr = ServerSSM(self, remoteDevice) + tr = ServerSSM(self, apdu.pduSource) # add it to our transactions to track it self.serverTransactions.append(tr) @@ -1178,7 +1242,7 @@ def confirmation(self, pdu): # find the client transaction this is acking for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1190,7 +1254,7 @@ def confirmation(self, pdu): # find the transaction being aborted if apdu.apduSrv: for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1199,7 +1263,7 @@ def confirmation(self, pdu): tr.confirmation(apdu) else: for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1211,7 +1275,7 @@ def confirmation(self, pdu): # find the transaction being aborted if apdu.apduSrv: for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1220,7 +1284,7 @@ def confirmation(self, pdu): tr.confirmation(apdu) else: for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1264,19 +1328,15 @@ def sap_indication(self, apdu): 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): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.pdu_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) + tr = ClientSSM(self, apdu.pduDestination) if _debug: StateMachineAccessPoint._debug(" - client segmentation state machine: %r", tr) # add it to our transactions to track it @@ -1300,7 +1360,7 @@ def sap_confirmation(self, apdu): 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): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.pdu_address): break else: return @@ -1327,23 +1387,26 @@ def indication(self, apdu): if _debug: ApplicationServiceAccessPoint._debug("indication %r", apdu) if isinstance(apdu, ConfirmedRequestPDU): + # assume no errors found + error_found = None + + # look up the class associated with the service 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 + error_found = UnrecognizedService() - try: - xpdu = atype() - xpdu.decode(apdu) - except RejectException as err: - ApplicationServiceAccessPoint._debug(" - decoding reject: %r", err) - error_found = err - except AbortException as err: - ApplicationServiceAccessPoint._debug(" - decoding abort: %r", err) - error_found = err + # no error so far, keep going + if not error_found: + try: + xpdu = atype() + xpdu.decode(apdu) + except RejectException as err: + ApplicationServiceAccessPoint._debug(" - decoding reject: %r", err) + error_found = err + except AbortException as err: + ApplicationServiceAccessPoint._debug(" - decoding abort: %r", err) + error_found = err # no error so far, keep going if not error_found: diff --git a/py27/bacpypes/bvllservice.py b/py27/bacpypes/bvllservice.py index 4c2cc182..a1147149 100755 --- a/py27/bacpypes/bvllservice.py +++ b/py27/bacpypes/bvllservice.py @@ -542,34 +542,38 @@ def confirmation(self, pdu): return - elif isinstance(pdu, OriginalUnicastNPDU): + if isinstance(pdu, OriginalUnicastNPDU): # build a vanilla PDU xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) # send it upstream self.response(xpdu) - return - - # check the BBMD registration status, we may not be registered - if self.registrationStatus != 0: - if _debug: BIPForeign._debug(" - packet dropped, unregistered") - return - if isinstance(pdu, ReadBroadcastDistributionTableAck): - # send this to the service access point - self.sap_response(pdu) + elif isinstance(pdu, ForwardedNPDU): + # check the BBMD registration status, we may not be registered + if self.registrationStatus != 0: + if _debug: BIPForeign._debug(" - packet dropped, unregistered") + return - elif isinstance(pdu, ReadForeignDeviceTableAck): - # send this to the service access point - self.sap_response(pdu) + # make sure the forwarded PDU from the bbmd + if pdu.pduSource != self.bbmdAddress: + if _debug: BIPForeign._debug(" - packet dropped, not from the BBMD") + return - elif isinstance(pdu, ForwardedNPDU): # build a PDU with the source from the real source xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) # send it upstream self.response(xpdu) + elif isinstance(pdu, ReadBroadcastDistributionTableAck): + # send this to the service access point + self.sap_response(pdu) + + elif isinstance(pdu, ReadForeignDeviceTableAck): + # send this to the service access point + self.sap_response(pdu) + elif isinstance(pdu, WriteBroadcastDistributionTable): # build a response xpdu = Result(code=0x0010, user_data=pdu.pduUserData) @@ -618,6 +622,9 @@ def confirmation(self, pdu): # send it downstream self.request(xpdu) + elif isinstance(pdu, OriginalBroadcastNPDU): + if _debug: BIPForeign._debug(" - packet dropped") + else: BIPForeign._warning("invalid pdu type: %s", type(pdu)) @@ -692,7 +699,7 @@ def indication(self, pdu): # make an original unicast PDU xpdu = OriginalUnicastNPDU(pdu, user_data=pdu.pduUserData) xpdu.pduDestination = pdu.pduDestination - if _debug: BIPBBMD._debug(" - xpdu: %r", xpdu) + if _debug: BIPBBMD._debug(" - original unicast xpdu: %r", xpdu) # send it downstream self.request(xpdu) @@ -715,13 +722,13 @@ def indication(self, pdu): for bdte in self.bbmdBDT: if bdte != self.bbmdAddress: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) else: @@ -737,8 +744,9 @@ def confirmation(self, pdu): elif isinstance(pdu, WriteBroadcastDistributionTable): # build a response - xpdu = Result(code=99, user_data=pdu.pduUserData) + xpdu = Result(code=0x0010, user_data=pdu.pduUserData) xpdu.pduDestination = pdu.pduSource + if _debug: BIPBBMD._debug(" - xpdu: %r", xpdu) # send it downstream self.request(xpdu) @@ -757,27 +765,38 @@ def confirmation(self, pdu): self.sap_response(pdu) elif isinstance(pdu, ForwardedNPDU): - # build a PDU with the source from the real source - xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # build a forwarded NPDU to send out xpdu = ForwardedNPDU(pdu.bvlciAddress, pdu, destination=None, user_data=pdu.pduUserData) if _debug: BIPBBMD._debug(" - forwarded xpdu: %r", xpdu) - # look for self as first entry in the BDT - if self.bbmdBDT and (self.bbmdBDT[0] == self.bbmdAddress): - xpdu.pduDestination = LocalBroadcast() - if _debug: BIPBBMD._debug(" - local broadcast") - self.request(xpdu) + # if this was unicast to us, do next hop + if pdu.pduDestination.addrType == Address.localStationAddr: + if _debug: BIPBBMD._debug(" - unicast message") + + # if this BBMD is listed in its BDT, send a local broadcast + if self.bbmdAddress in self.bbmdBDT: + xpdu.pduDestination = LocalBroadcast() + if _debug: BIPBBMD._debug(" - local broadcast") + self.request(xpdu) + + elif pdu.pduDestination.addrType == Address.localBroadcastAddr: + if _debug: BIPBBMD._debug(" - directed broadcast message") + + else: + BIPBBMD._warning("invalid destination address: %r", pdu.pduDestination) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) elif isinstance(pdu, RegisterForeignDevice): @@ -816,12 +835,13 @@ def confirmation(self, pdu): self.request(xpdu) elif isinstance(pdu, DistributeBroadcastToNetwork): - # build a PDU with a local broadcast address - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # build a forwarded NPDU to send out xpdu = ForwardedNPDU(pdu.pduSource, pdu, user_data=pdu.pduUserData) @@ -831,35 +851,37 @@ def confirmation(self, pdu): for bdte in self.bbmdBDT: if bdte == self.bbmdAddress: xpdu.pduDestination = LocalBroadcast() - if _debug: BIPBBMD._debug(" - local broadcast") + if _debug: BIPBBMD._debug(" - local broadcast") self.request(xpdu) else: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the other registered foreign devices for fdte in self.bbmdFDT: if fdte.fdAddress != pdu.pduSource: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) elif isinstance(pdu, OriginalUnicastNPDU): - # build a vanilla PDU - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) elif isinstance(pdu, OriginalBroadcastNPDU): - # build a PDU with a local broadcast address - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # make a forwarded PDU xpdu = ForwardedNPDU(pdu.pduSource, pdu, user_data=pdu.pduUserData) @@ -869,13 +891,13 @@ def confirmation(self, pdu): for bdte in self.bbmdBDT: if bdte != self.bbmdAddress: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) else: @@ -925,7 +947,7 @@ def delete_foreign_device_table_entry(self, addr): del self.bbmdFDT[i] break else: - stat = 99 ### entry not found + stat = 0x0050 ### entry not found # return status return stat @@ -952,10 +974,6 @@ def add_peer(self, addr): else: raise TypeError("addr must be a string or an Address") - # if it's this BBMD, make it the first one - if self.bbmdBDT and (addr == self.bbmdAddress): - raise RuntimeError("add self to BDT as first address") - # see if it's already there for bdte in self.bbmdBDT: if addr == bdte: diff --git a/py27/bacpypes/local/device.py b/py27/bacpypes/local/device.py index f1aef729..5256c28f 100644 --- a/py27/bacpypes/local/device.py +++ b/py27/bacpypes/local/device.py @@ -1,8 +1,10 @@ #!/usr/bin/env python -from ..debugging import bacpypes_debugging, ModuleLogger +from ..debugging import bacpypes_debugging, ModuleLogger, xtob -from ..primitivedata import Date, Time, ObjectIdentifier +from ..primitivedata import Null, Boolean, Unsigned, Integer, Real, Double, \ + OctetString, CharacterString, BitString, Enumerated, Date, Time, \ + ObjectIdentifier from ..constructeddata import ArrayOf from ..basetypes import ServicesSupported @@ -107,21 +109,113 @@ class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): def __init__(self, **kwargs): if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) - # fill in default property values not in kwargs - for attr, value in LocalDeviceObject.defaultProperties.items(): - if attr not in kwargs: - kwargs[attr] = value - - for key, value in kwargs.items(): - if key.startswith("_"): - setattr(self, key, value) - del kwargs[key] + # start with an empty dictionary of device object properties + init_args = {} + ini_arg = kwargs.get('ini', None) + if _debug: LocalDeviceObject._debug(" - ini_arg: %r", dir(ini_arg)) - # check for registration + # check for registration as a keyword parameter or in the INI file if self.__class__ not in registered_object_types.values(): - if 'vendorIdentifier' not in kwargs: + if _debug: LocalDeviceObject._debug(" - unregistered") + + vendor_identifier = kwargs.get('vendorIdentifier', None) + if _debug: LocalDeviceObject._debug(" - keyword vendor identifier: %r", vendor_identifier) + + if vendor_identifier is None: + vendor_identifier = getattr(ini_arg, 'vendoridentifier', None) + if _debug: LocalDeviceObject._debug(" - INI vendor identifier: %r", vendor_identifier) + + if vendor_identifier is None: raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") - register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) + + register_object_type(self.__class__, vendor_id=vendor_identifier) + + # look for properties, fill in values from the keyword arguments or + # the INI parameter (converted to a proper value) if it was provided + for propid, prop in self._properties.items(): + # special processing for object identifier + if propid == 'objectIdentifier': + continue + + # use keyword argument if it was provided + if propid in kwargs: + prop_value = kwargs[propid] + else: + prop_value = getattr(ini_arg, propid.lower(), None) + if prop_value is None: + continue + + prop_datatype = prop.datatype + + if issubclass(prop_datatype, Null): + if prop_value != "Null": + raise ValueError("invalid null property value: %r" % (propid,)) + prop_value = None + + elif issubclass(prop_datatype, Boolean): + prop_value = prop_value.lower() + if prop_value not in ('true', 'false', 'set', 'reset'): + raise ValueError("invalid boolean property value: %r" % (propid,)) + prop_value = prop_value in ('true', 'set') + + elif issubclass(prop_datatype, (Unsigned, Integer)): + try: + prop_value = int(prop_value) + except ValueError: + raise ValueError("invalid unsigned or integer property value: %r" % (propid,)) + + elif issubclass(prop_datatype, (Real, Double)): + try: + prop_value = float(prop_value) + except ValueError: + raise ValueError("invalid real or double property value: %r" % (propid,)) + + elif issubclass(prop_datatype, OctetString): + try: + prop_value = xtob(prop_value) + except: + raise ValueError("invalid octet string property value: %r" % (propid,)) + + elif issubclass(prop_datatype, CharacterString): + pass + + elif issubclass(prop_datatype, BitString): + try: + bstr, prop_value = prop_value, [] + for b in bstr: + if b not in ('0', '1'): + raise ValueError + prop_value.append(int(b)) + except: + raise ValueError("invalid bit string property value: %r" % (propid,)) + + elif issubclass(prop_datatype, Enumerated): + pass + + else: + raise ValueError("cannot interpret %r INI paramter" % (propid,)) + if _debug: LocalDeviceObject._debug(" - property %r: %r", propid, prop_value) + + # at long last + init_args[propid] = prop_value + + # check for object identifier as a keyword parameter or in the INI file, + # and it might be just an int, so make it a tuple if necessary + if 'objectIdentifier' in kwargs: + object_identifier = kwargs['objectIdentifier'] + if isinstance(object_identifier, (int, long)): + object_identifier = ('device', object_identifier) + elif hasattr(ini_arg, 'objectidentifier'): + object_identifier = ('device', int(getattr(ini_arg, 'objectidentifier'))) + else: + raise RuntimeError("objectIdentifier is required") + init_args['objectIdentifier'] = object_identifier + if _debug: LocalDeviceObject._debug(" - object identifier: %r", object_identifier) + + # fill in default property values not in init_args + for attr, value in LocalDeviceObject.defaultProperties.items(): + if attr not in init_args: + init_args[attr] = value # check for properties this class implements if 'localDate' in kwargs: @@ -131,27 +225,23 @@ def __init__(self, **kwargs): if 'protocolServicesSupported' in kwargs: raise RuntimeError("protocolServicesSupported is provided by LocalDeviceObject and cannot be overridden") - # the object identifier is required for the object list - if 'objectIdentifier' not in kwargs: - raise RuntimeError("objectIdentifier is required") - - # coerce the object identifier - object_identifier = kwargs['objectIdentifier'] - if isinstance(object_identifier, (int, long)): - object_identifier = ('device', object_identifier) - # the object list is provided if 'objectList' in kwargs: raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") - kwargs['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) + init_args['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) # check for a minimum value - if kwargs['maxApduLengthAccepted'] < 50: + if init_args['maxApduLengthAccepted'] < 50: raise ValueError("invalid max APDU length accepted") # dump the updated attributes - if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) + if _debug: LocalDeviceObject._debug(" - init_args: %r", init_args) # proceed as usual - super(LocalDeviceObject, self).__init__(**kwargs) + super(LocalDeviceObject, self).__init__(**init_args) + + # pass along special property values that are not BACnet properties + for key, value in kwargs.items(): + if key.startswith("_"): + setattr(self, key, value) diff --git a/py27/bacpypes/object.py b/py27/bacpypes/object.py index dc09e736..ac39cc05 100755 --- a/py27/bacpypes/object.py +++ b/py27/bacpypes/object.py @@ -1422,7 +1422,7 @@ class EventEnrollmentObject(Object): , ReadableProperty('eventTimeStamps', ArrayOf(TimeStamp)) , OptionalProperty('eventMessageTexts', ArrayOf(CharacterString)) , OptionalProperty('eventMessageTextsConfig', ArrayOf(CharacterString)) - , OptionalProperty('eventDetectionEnable', Boolean) + , ReadableProperty('eventDetectionEnable', Boolean) , OptionalProperty('eventAlgorithmInhibitRef', ObjectPropertyReference) , OptionalProperty('eventAlgorithmInhibit', Boolean) , OptionalProperty('timeDelayNormal', Unsigned) diff --git a/py27/bacpypes/task.py b/py27/bacpypes/task.py index c4fec155..e3f405ce 100755 --- a/py27/bacpypes/task.py +++ b/py27/bacpypes/task.py @@ -8,6 +8,7 @@ from time import time as _time from heapq import heapify, heappush, heappop +import itertools from .singleton import SingletonLogging from .debugging import DebugContents, Logging, ModuleLogger, bacpypes_debugging @@ -276,6 +277,9 @@ def __init__(self): # task manager is this instance _task_manager = self + # unique sequence counter for tasks scheduled at the same time + self.counter = itertools.count() + # there may be tasks created that couldn't be scheduled # because a task manager wasn't created yet. if _unscheduled_tasks: @@ -300,7 +304,7 @@ def install_task(self, task): self.suspend_task(task) # save this in the task list - heappush( self.tasks, (task.taskTime, task) ) + heappush( self.tasks, (task.taskTime, next(self.counter), task) ) if _debug: TaskManager._debug(" - tasks: %r", self.tasks) task.isScheduled = True @@ -313,7 +317,7 @@ def suspend_task(self, task): if _debug: TaskManager._debug("suspend_task %r", task) # remove this guy - for i, (when, curtask) in enumerate(self.tasks): + for i, (when, n, curtask) in enumerate(self.tasks): if task is curtask: if _debug: TaskManager._debug(" - task found") del self.tasks[i] @@ -348,7 +352,7 @@ def get_next_task(self): if self.tasks: # look at the first task - when, nxttask = self.tasks[0] + when, n, nxttask = self.tasks[0] if when <= now: # pull it off the list and mark that it's no longer scheduled heappop(self.tasks) @@ -356,7 +360,7 @@ def get_next_task(self): task.isScheduled = False if self.tasks: - when, nxttask = self.tasks[0] + when, n, nxttask = self.tasks[0] # peek at the next task, return how long to wait delta = max(when - now, 0.0) else: diff --git a/py27/bacpypes/vlan.py b/py27/bacpypes/vlan.py index ead2d03a..5908e898 100755 --- a/py27/bacpypes/vlan.py +++ b/py27/bacpypes/vlan.py @@ -32,6 +32,7 @@ def __init__(self, name='', broadcast_address=None, drop_percent=0.0): self.name = name self.nodes = [] + self.broadcast_address = broadcast_address self.drop_percent = drop_percent @@ -161,9 +162,9 @@ class IPNetwork(Network): ('1.2.3.255', 5) and the other nodes must have the same tuple. """ - def __init__(self): + def __init__(self, name=''): if _debug: IPNetwork._debug("__init__") - Network.__init__(self) + Network.__init__(self, name=name) def add_node(self, node): if _debug: IPNetwork._debug("add_node %r", node) @@ -213,11 +214,12 @@ def __init__(self, addr, lan=None, promiscuous=False, spoofing=False, sid=None): @bacpypes_debugging class IPRouterNode(Client): - def __init__(self, router, addr, lan=None): + def __init__(self, router, addr, lan): if _debug: IPRouterNode._debug("__init__ %r %r lan=%r", router, addr, lan) - # save the reference to the router + # save the references to the router for packets and the lan for debugging self.router = router + self.lan = lan # make ourselves an IPNode and bind to it self.node = IPNode(addr, lan=lan, promiscuous=True, spoofing=True) @@ -238,6 +240,10 @@ def process_pdu(self, pdu): # pass it downstream self.request(pdu) + def __repr__(self): + return "<%s for %s>" % (self.__class__.__name__, self.lan.name) + + # # IPRouter # diff --git a/py34/bacpypes/__init__.py b/py34/bacpypes/__init__.py index a77e8be9..084ca281 100755 --- a/py34/bacpypes/__init__.py +++ b/py34/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.17.0' +__version__ = '0.17.1' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py34/bacpypes/apdu.py b/py34/bacpypes/apdu.py index 10706aa2..c19a811e 100755 --- a/py34/bacpypes/apdu.py +++ b/py34/bacpypes/apdu.py @@ -56,38 +56,51 @@ def register_error_type(klass): # encode_max_segments_accepted/decode_max_segments_accepted # +_max_segments_accepted_encoding = [ + None, 2, 4, 8, 16, 32, 64, None, + ] + def encode_max_segments_accepted(arg): """Encode the maximum number of segments the device will accept, Section - 20.1.2.4""" - w = 0 - while (arg and not arg & 1): - w += 1 - arg = (arg >> 1) - return w + 20.1.2.4, and if the device says it can only accept one segment it shouldn't + say that it supports segmentation!""" + # unspecified + if not arg: + return 0 + + if arg > 64: + return 7 + + # the largest number not greater than the arg + for i in range(6, 0, -1): + if _max_segments_accepted_encoding[i] <= arg: + return i + + raise ValueError("invalid max max segments accepted: {0}".format(arg)) def decode_max_segments_accepted(arg): """Decode the maximum number of segments the device will accept, Section 20.1.2.4""" - return arg and (1 << arg) or None + return _max_segments_accepted_encoding[arg] # # encode_max_apdu_length_accepted/decode_max_apdu_length_accepted # -_max_apdu_response_encoding = [50, 128, 206, 480, 1024, 1476, None, None, +_max_apdu_length_encoding = [50, 128, 206, 480, 1024, 1476, None, None, None, None, None, None, None, None, None, None] def encode_max_apdu_length_accepted(arg): """Return the encoding of the highest encodable value less than the value of the arg.""" for i in range(5, -1, -1): - if (arg >= _max_apdu_response_encoding[i]): + if (arg >= _max_apdu_length_encoding[i]): return i raise ValueError("invalid max APDU length accepted: {0}".format(arg)) def decode_max_apdu_length_accepted(arg): - v = _max_apdu_response_encoding[arg] + v = _max_apdu_length_encoding[arg] if not v: raise ValueError("invalid max APDU length accepted: {0}".format(arg)) @@ -174,7 +187,7 @@ def encode(self, pdu): if self.apduSA: buff += 0x02 pdu.put(buff) - pdu.put((encode_max_segments_accepted(self.apduMaxSegs) << 4) + encode_max_apdu_length_accepted(self.apduMaxResp)) + pdu.put((self.apduMaxSegs << 4) + self.apduMaxResp) pdu.put(self.apduInvokeID) if self.apduSeg: pdu.put(self.apduSeq) @@ -255,8 +268,8 @@ def decode(self, pdu): self.apduMor = ((buff & 0x04) != 0) self.apduSA = ((buff & 0x02) != 0) buff = pdu.get() - self.apduMaxSegs = decode_max_segments_accepted( (buff >> 4) & 0x07 ) - self.apduMaxResp = decode_max_apdu_length_accepted( buff & 0x0F ) + self.apduMaxSegs = (buff >> 4) & 0x07 + self.apduMaxResp = buff & 0x0F self.apduInvokeID = pdu.get() if self.apduSeg: self.apduSeq = pdu.get() @@ -1301,7 +1314,7 @@ class LifeSafetyOperationRequest(ConfirmedRequestSequence): [ Element('requestingProcessIdentifier', Unsigned, 0) , Element('requestingSource', CharacterString, 1) , Element('request', LifeSafetyOperation, 2) - , Element('objectIdentifier', ObjectIdentifier, 3) + , Element('objectIdentifier', ObjectIdentifier, 3, True) ] register_confirmed_request_type(LifeSafetyOperationRequest) @@ -1485,7 +1498,7 @@ class RemoveListElementRequest(ConfirmedRequestSequence): sequenceElements = \ [ Element('objectIdentifier', ObjectIdentifier, 0) , Element('propertyIdentifier', PropertyIdentifier, 1) - , Element('propertyArrayIndex', Unsigned, 2) + , Element('propertyArrayIndex', Unsigned, 2, True) , Element('listOfElements', Any, 3) ] diff --git a/py34/bacpypes/app.py b/py34/bacpypes/app.py index a1081497..1548a3b4 100755 --- a/py34/bacpypes/app.py +++ b/py34/bacpypes/app.py @@ -26,7 +26,8 @@ # for computing protocol services supported from .apdu import confirmed_request_types, unconfirmed_request_types, \ - ConfirmedServiceChoice, UnconfirmedServiceChoice + ConfirmedServiceChoice, UnconfirmedServiceChoice, \ + IAmRequest from .basetypes import ServicesSupported # basic services @@ -54,16 +55,16 @@ class DeviceInfo(DebugContents): 'maxSegmentsAccepted', ) - def __init__(self): + def __init__(self, device_identifier, address): # this information is from an IAmRequest - self.deviceIdentifier = None # device identifier - self.address = None # LocalStation or RemoteStation + self.deviceIdentifier = device_identifier + self.address = address + self.maxApduLengthAccepted = 1024 # maximum APDU device will accept self.segmentationSupported = 'noSegmentation' # normally no segmentation + self.maxSegmentsAccepted = None # None iff no segmentation self.vendorID = None # vendor identifier - - self.maxNpduLength = 1497 # maximum we can send in transit - self.maxSegmentsAccepted = None # value for proposed/actual window size + self.maxNpduLength = None # maximum we can send in transit (see 19.4) # # DeviceInfoCache @@ -72,127 +73,134 @@ def __init__(self): @bacpypes_debugging class DeviceInfoCache: - def __init__(self): + def __init__(self, device_info_class=DeviceInfo): if _debug: DeviceInfoCache._debug("__init__") + # a little error checking + if not issubclass(device_info_class, DeviceInfo): + raise ValueError("not a DeviceInfo subclass: %r" % (device_info_class,)) + # empty cache self.cache = {} + # class for new records + self.device_info_class = device_info_class + def has_device_info(self, key): """Return true iff cache has information about the device.""" if _debug: DeviceInfoCache._debug("has_device_info %r", key) return key in self.cache - def add_device_info(self, apdu): + def iam_device_info(self, apdu): """Create a device information record based on the contents of an IAmRequest and put it in the cache.""" - if _debug: DeviceInfoCache._debug("add_device_info %r", apdu) + if _debug: DeviceInfoCache._debug("iam_device_info %r", apdu) - # get the existing cache record by identifier - info = self.get_device_info(apdu.iAmDeviceIdentifier[1]) - if _debug: DeviceInfoCache._debug(" - info: %r", info) + # make sure the apdu is an I-Am + if not isinstance(apdu, IAmRequest): + raise ValueError("not an IAmRequest: %r" % (apdu,)) - # update existing record - if info: - if (info.address == apdu.pduSource): - return + # get the device instance + device_instance = apdu.iAmDeviceIdentifier[1] - info.address = apdu.pduSource - else: - # get the existing record by address (creates a new record) - info = self.get_device_info(apdu.pduSource) - if _debug: DeviceInfoCache._debug(" - info: %r", info) + # get the existing cache record if it exists + device_info = self.cache.get(device_instance, None) - info.deviceIdentifier = apdu.iAmDeviceIdentifier[1] + # maybe there is a record for this address + if not device_info: + device_info = self.cache.get(apdu.pduSource, None) - # update the rest of the values - info.maxApduLengthAccepted = apdu.maxAPDULengthAccepted - info.segmentationSupported = apdu.segmentationSupported - info.vendorID = apdu.vendorID + # make a new one using the class provided + if not device_info: + device_info = self.device_info_class(device_instance, apdu.pduSource) - # say this is an updated record - self.update_device_info(info) + # jam in the correct values + device_info.deviceIdentifier = device_instance + device_info.address = apdu.pduSource + device_info.maxApduLengthAccepted = apdu.maxAPDULengthAccepted + device_info.segmentationSupported = apdu.segmentationSupported + device_info.vendorID = apdu.vendorID + + # tell the cache this is an updated record + self.update_device_info(device_info) def get_device_info(self, key): - """Return the known information about the device. If the key is the - address of an unknown device, build a generic device information record - add put it in the cache.""" if _debug: DeviceInfoCache._debug("get_device_info %r", key) - if isinstance(key, int): - current_info = self.cache.get(key, None) + # get the info if it's there + device_info = self.cache.get(key, None) + if _debug: DeviceInfoCache._debug(" - device_info: %r", device_info) - elif not isinstance(key, Address): - raise TypeError("key must be integer or an address") + return device_info - elif key.addrType not in (Address.localStationAddr, Address.remoteStationAddr): - raise TypeError("address must be a local or remote station") - - else: - current_info = self.cache.get(key, None) - if not current_info: - current_info = DeviceInfo() - current_info.address = key - current_info._cache_keys = (None, key) - current_info._ref_count = 1 - - self.cache[key] = current_info - else: - if _debug: DeviceInfoCache._debug(" - reference bump") - current_info._ref_count += 1 - - if _debug: DeviceInfoCache._debug(" - current_info: %r", current_info) - - return current_info - - def update_device_info(self, info): + def update_device_info(self, device_info): """The application has updated one or more fields in the device information record and the cache needs to be updated to reflect the changes. If this is a cached version of a persistent record then this is the opportunity to update the database.""" - if _debug: DeviceInfoCache._debug("update_device_info %r", info) + if _debug: DeviceInfoCache._debug("update_device_info %r", device_info) - cache_id, cache_address = info._cache_keys + # give this a reference count if it doesn't have one + if not hasattr(device_info, '_ref_count'): + device_info._ref_count = 0 - if (cache_id is not None) and (info.deviceIdentifier != cache_id): + # get the current keys + cache_id, cache_address = getattr(device_info, '_cache_keys', (None, None)) + + if (cache_id is not None) and (device_info.deviceIdentifier != cache_id): if _debug: DeviceInfoCache._debug(" - device identifier updated") # remove the old reference, add the new one del self.cache[cache_id] - self.cache[info.deviceIdentifier] = info - - cache_id = info.deviceIdentifier + self.cache[device_info.deviceIdentifier] = device_info - if (cache_address is not None) and (info.address != cache_address): + if (cache_address is not None) and (device_info.address != cache_address): if _debug: DeviceInfoCache._debug(" - device address updated") # remove the old reference, add the new one del self.cache[cache_address] - self.cache[info.address] = info - - cache_address = info.address + self.cache[device_info.address] = device_info # update the keys - info._cache_keys = (cache_id, cache_address) + device_info._cache_keys = (device_info.deviceIdentifier, device_info.address) + + def acquire(self, key): + """Return the known information about the device and mark the record + as being used by a segmenation state machine.""" + if _debug: DeviceInfoCache._debug("acquire %r", key) + + if isinstance(key, int): + device_info = self.cache.get(key, None) + + elif not isinstance(key, Address): + raise TypeError("key must be integer or an address") + + elif key.addrType not in (Address.localStationAddr, Address.remoteStationAddr): + raise TypeError("address must be a local or remote station") + + else: + device_info = self.cache.get(key, None) + + if device_info: + if _debug: DeviceInfoCache._debug(" - reference bump") + device_info._ref_count += 1 + + if _debug: DeviceInfoCache._debug(" - device_info: %r", device_info) - def release_device_info(self, info): + return device_info + + def release(self, device_info): """This function is called by the segmentation state machine when it has finished with the device information.""" - if _debug: DeviceInfoCache._debug("release_device_info %r", info) + if _debug: DeviceInfoCache._debug("release %r", device_info) # this information record might be used by more than one SSM - if info._ref_count > 1: - if _debug: DeviceInfoCache._debug(" - multiple references") - info._ref_count -= 1 - return + if device_info._ref_count == 0: + raise RuntimeError("reference count") - cache_id, cache_address = info._cache_keys - if cache_id is not None: - del self.cache[cache_id] - if cache_address is not None: - del self.cache[cache_address] - if _debug: DeviceInfoCache._debug(" - released") + # decrement the reference count + device_info._ref_count -= 1 # # Application @@ -260,9 +268,9 @@ def add_object(self, obj): # make sure it hasn't already been defined if object_name in self.objectName: - raise RuntimeError("already an object with name {0!r}".format(object_name)) + raise RuntimeError("already an object with name %r" % (object_name,)) if object_identifier in self.objectIdentifier: - raise RuntimeError("already an object with identifier {0!r}".format(object_identifier)) + raise RuntimeError("already an object with identifier %r" % (object_identifier,)) # now put it in local dictionaries self.objectName[object_name] = obj diff --git a/py34/bacpypes/appservice.py b/py34/bacpypes/appservice.py index 4a3b65da..43316f13 100755 --- a/py34/bacpypes/appservice.py +++ b/py34/bacpypes/appservice.py @@ -12,12 +12,14 @@ from .task import OneShotTask from .pdu import Address -from .apdu import AbortPDU, AbortReason, ComplexAckPDU, \ +from .apdu import encode_max_segments_accepted, decode_max_segments_accepted, \ + encode_max_apdu_length_accepted, decode_max_apdu_length_accepted, \ + 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 +from .errors import RejectException, AbortException, UnrecognizedService # some debugging _debug = 0 @@ -45,19 +47,23 @@ class SSM(OneShotTask, DebugContents): , 'SEGMENTED_RESPONSE', 'SEGMENTED_CONFIRMATION', 'COMPLETED', 'ABORTED' ] - _debug_contents = ('ssmSAP', 'localDevice', 'remoteDevice', 'invokeID' + _debug_contents = ('ssmSAP', 'localDevice', 'device_info', 'invokeID' , 'state', 'segmentAPDU', 'segmentSize', 'segmentCount', 'maxSegmentsAccepted' , 'retryCount', 'segmentRetryCount', 'sentAllSegments', 'lastSequenceNumber' , 'initialSequenceNumber', 'actualWindowSize', 'proposedWindowSize' ) - def __init__(self, sap, remoteDevice): + def __init__(self, sap, pdu_address): """Common parts for client and server segmentation.""" - if _debug: SSM._debug("__init__ %r %r", sap, remoteDevice) + if _debug: SSM._debug("__init__ %r %r", sap, pdu_address) OneShotTask.__init__(self) self.ssmSAP = sap # service access point - self.remoteDevice = remoteDevice # remote device information, a DeviceInfo instance + + # save the address and get the device information + self.pdu_address = pdu_address + self.device_info = sap.deviceInfoCache.get_device_info(pdu_address) + self.invokeID = None # invoke ID self.state = IDLE # initial state @@ -71,11 +77,17 @@ def __init__(self, sap, remoteDevice): 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 + # local device object provides these or SAP provides defaults, make + # copies here so they are consistent throughout the transaction but + # they could change from one transaction to the next + self.numberOfApduRetries = getattr(sap.localDevice, 'numberOfApduRetries', sap.numberOfApduRetries) + self.apduTimeout = getattr(sap.localDevice, 'apduTimeout', sap.apduTimeout) + + self.segmentationSupported = getattr(sap.localDevice, 'segmentationSupported', sap.segmentationSupported) + self.segmentTimeout = getattr(sap.localDevice, 'segmentTimeout', sap.segmentTimeout) + self.maxSegmentsAccepted = getattr(sap.localDevice, 'maxSegmentsAccepted', sap.maxSegmentsAccepted) + self.maxApduLengthAccepted = getattr(sap.localDevice, 'maxApduLengthAccepted', sap.maxApduLengthAccepted) def start_timer(self, msecs): if _debug: SSM._debug("start_timer %r", msecs) @@ -153,12 +165,12 @@ def get_segment(self, indx): segAPDU = ConfirmedRequestPDU(self.segmentAPDU.apduService) - segAPDU.apduMaxSegs = self.maxSegmentsAccepted - segAPDU.apduMaxResp = self.ssmSAP.maxApduLengthAccepted - segAPDU.apduInvokeID = self.invokeID; + segAPDU.apduMaxSegs = encode_max_segments_accepted(self.maxSegmentsAccepted) + segAPDU.apduMaxResp = encode_max_apdu_length_accepted(self.maxApduLengthAccepted) + segAPDU.apduInvokeID = self.invokeID # segmented response accepted? - segAPDU.apduSA = self.ssmSAP.segmentationSupported in ('segmentedReceive', 'segmentedBoth') + segAPDU.apduSA = self.segmentationSupported in ('segmentedReceive', 'segmentedBoth') if _debug: SSM._debug(" - segmented response accepted: %r", segAPDU.apduSA) elif self.segmentAPDU.apduType == ComplexAckPDU.pduType: @@ -172,14 +184,21 @@ def get_segment(self, indx): segAPDU.pduUserData = self.segmentAPDU.pduUserData # make sure the destination is set - segAPDU.pduDestination = self.remoteDevice.address + segAPDU.pduDestination = self.pdu_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 + + # first segment sends proposed window size, rest get actual + if indx == 0: + if _debug: SSM._debug(" - proposedWindowSize: %r", self.proposedWindowSize) + segAPDU.apduWin = self.proposedWindowSize + else: + if _debug: SSM._debug(" - actualWindowSize: %r", self.actualWindowSize) + segAPDU.apduWin = self.actualWindowSize else: segAPDU.apduSeg = False segAPDU.apduMor = False @@ -211,10 +230,11 @@ def in_window(self, seqA, seqB): return rslt - def FillWindow(self, seqNum): + def fill_window(self, seqNum): """This function sends all of the packets necessary to fill out the segmentation window.""" - if _debug: SSM._debug("FillWindow %r", seqNum) + if _debug: SSM._debug("fill_window %r", seqNum) + if _debug: SSM._debug(" - actualWindowSize: %r", self.actualWindowSize) for ix in range(self.actualWindowSize): apdu = self.get_segment(seqNum + ix) @@ -234,13 +254,18 @@ def FillWindow(self, seqNum): @bacpypes_debugging class ClientSSM(SSM): - def __init__(self, sap, remoteDevice): - if _debug: ClientSSM._debug("__init__ %s %r", sap, remoteDevice) - SSM.__init__(self, sap, remoteDevice) + def __init__(self, sap, pdu_address): + if _debug: ClientSSM._debug("__init__ %s %r", sap, pdu_address) + SSM.__init__(self, sap, pdu_address) # initialize the retry count self.retryCount = 0 + # acquire the device info + if self.device_info: + if _debug: ClientSSM._debug(" - acquire device information") + self.ssmSAP.deviceInfoCache.acquire(self.device_info) + 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) @@ -253,8 +278,10 @@ def set_state(self, newState, timer=0): 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) + # release the device info + if self.device_info: + if _debug: ClientSSM._debug(" - release device information") + self.ssmSAP.deviceInfoCache.release(self.device_info) def request(self, apdu): """This function is called by client transaction functions when it wants @@ -263,7 +290,7 @@ def request(self, apdu): # make sure it has a good source and destination apdu.pduSource = None - apdu.pduDestination = self.remoteDevice.address + apdu.pduDestination = self.pdu_address # send it via the device self.ssmSAP.request(apdu) @@ -280,26 +307,27 @@ def indication(self, apdu): # 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) + # if the max apdu length of the server isn't known, assume that it + # is the same size as our own and will be the segment size + if (not self.device_info) or (self.device_info.maxApduLengthAccepted is None): + self.segmentSize = self.maxApduLengthAccepted - # the maximum number of segments acceptable in the reply - if apdu.apduMaxSegs is not None: - # this request overrides the default - self.maxSegmentsAccepted = apdu.apduMaxSegs + # if the max npdu length of the server isn't known, assume that it + # is the same as the max apdu length accepted + elif self.device_info.maxNpduLength is None: + self.segmentSize = self.device_info.maxApduLengthAccepted + + # the segment size is the minimum of the size of the largest packet + # that can be delivered to the server and the largest it can accept + else: + self.segmentSize = min(self.device_info.maxNpduLength, self.device_info.maxApduLengthAccepted) + if _debug: ClientSSM._debug(" - segment size: %r", self.segmentSize) # save the invoke ID self.invokeID = apdu.apduInvokeID if _debug: ClientSSM._debug(" - invoke ID: %r", self.invokeID) - # compute the segment count ### minus the header? + # compute the segment count if not apdu.pduData: # always at least one segment self.segmentCount = 1 @@ -312,34 +340,49 @@ def indication(self, apdu): # make sure we support segmented transmit if we need to if self.segmentCount > 1: - if self.ssmSAP.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): + if self.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") + + if not self.device_info: + if _debug: ClientSSM._debug(" - no server info for segmentation support") + + elif self.device_info.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if _debug: ClientSSM._debug(" - server can't receive segmented requests") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return - ### check for APDUTooLong? + # make sure we dont exceed the number of segments in our request + # that the server said it was willing to accept + if not self.device_info: + if _debug: ClientSSM._debug(" - no server info for maximum number of segments") + + elif not self.device_info.maxSegmentsAccepted: + if _debug: ClientSSM._debug(" - server doesn't say maximum number of segments") + + elif self.segmentCount > self.device_info.maxSegmentsAccepted: + if _debug: ClientSSM._debug(" - server can't receive enough segments") + abort = self.abort(AbortReason.apduTooLong) + self.response(abort) + return # send out the first segment (or the whole thing) if self.segmentCount == 1: - # SendConfirmedUnsegmented + # unsegmented self.sentAllSegments = True self.retryCount = 0 - self.set_state(AWAIT_CONFIRMATION, self.ssmSAP.retryTimeout) + self.set_state(AWAIT_CONFIRMATION, self.apduTimeout) else: - # SendConfirmedSegmented + # segmented 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) + self.actualWindowSize = None # segment ack will set value + self.set_state(SEGMENTED_REQUEST, self.segmentTimeout) # deliver to the device self.request(self.get_segment(0)) @@ -350,7 +393,7 @@ def response(self, apdu): if _debug: ClientSSM._debug("response %r", apdu) # make sure it has a good source and destination - apdu.pduSource = self.remoteDevice.address + apdu.pduSource = self.pdu_address apdu.pduDestination = None # send it to the application @@ -407,29 +450,31 @@ def segmented_request(self, apdu): and receives an apdu.""" if _debug: ClientSSM._debug("segmented_request %r", apdu) - # client is ready for the next segment + # server is ready for the next segment if apdu.apduType == SegmentAckPDU.pduType: if _debug: ClientSSM._debug(" - segment ack") + # actual window size is provided by server + self.actualWindowSize = apdu.apduWin + # 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) + self.restart_timer(self.segmentTimeout) # final ack received? elif self.sentAllSegments: if _debug: ClientSSM._debug(" - all done sending request") - self.set_state(AWAIT_CONFIRMATION, self.ssmSAP.retryTimeout) + self.set_state(AWAIT_CONFIRMATION, self.apduTimeout) # 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) + self.fill_window(self.initialSequenceNumber) + self.restart_timer(self.segmentTimeout) # simple ack elif (apdu.apduType == SimpleAckPDU.pduType): @@ -452,6 +497,7 @@ def segmented_request(self, apdu): self.response(abort) # send it to the application elif not apdu.apduSeg: + # ack is not segmented self.set_state(COMPLETED) self.response(apdu) @@ -459,10 +505,11 @@ def segmented_request(self, apdu): # set the segmented response context self.set_segmentation_context(apdu) - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + # minimum of what the server is proposing and this client proposes + self.actualWindowSize = min(apdu.apduWin, self.proposedWindowSize) self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_CONFIRMATION, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_CONFIRMATION, self.segmentTimeout) # some kind of problem elif (apdu.apduType == ErrorPDU.pduType) or (apdu.apduType == RejectPDU.pduType) or (apdu.apduType == AbortPDU.pduType): @@ -479,12 +526,12 @@ def segmented_request_timeout(self): if _debug: ClientSSM._debug("segmented_request_timeout") # try again - if self.segmentRetryCount < self.ssmSAP.retryCount: + if self.segmentRetryCount < self.numberOfApduRetries: if _debug: ClientSSM._debug(" - retry segmented request") self.segmentRetryCount += 1 - self.start_timer(self.ssmSAP.segmentTimeout) - self.FillWindow(self.initialSequenceNumber) + self.start_timer(self.segmentTimeout) + self.fill_window(self.initialSequenceNumber) else: if _debug: ClientSSM._debug(" - abort, no response from the device") @@ -516,7 +563,7 @@ def await_confirmation(self, apdu): self.set_state(COMPLETED) self.response(apdu) - elif self.ssmSAP.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + elif self.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): if _debug: ClientSSM._debug(" - local device can't receive segmented messages") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) @@ -527,10 +574,10 @@ def await_confirmation(self, apdu): # set the segmented response context self.set_segmentation_context(apdu) - self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.maxSegmentsAccepted) + self.actualWindowSize = apdu.apduWin self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_CONFIRMATION, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_CONFIRMATION, self.segmentTimeout) # send back a segment ack segack = SegmentAckPDU( 0, 0, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) @@ -546,7 +593,7 @@ def await_confirmation(self, apdu): elif (apdu.apduType == SegmentAckPDU.pduType): if _debug: ClientSSM._debug(" - segment ack(!?)") - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) else: raise RuntimeError("invalid APDU (3)") @@ -554,9 +601,9 @@ def await_confirmation(self, apdu): 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) + if self.retryCount < self.numberOfApduRetries: + if _debug: ClientSSM._debug(" - no response, try again (%d < %d)", self.retryCount, self.numberOfApduRetries) + self.retryCount += 1 # save the retry count, indication acts like the request is coming # from the application so the retryCount gets re-initialized. @@ -594,8 +641,8 @@ def segmented_confirmation(self, apdu): 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.restart_timer(self.segmentTimeout) + segack = SegmentAckPDU(1, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) return @@ -610,7 +657,7 @@ def segmented_confirmation(self, apdu): if _debug: ClientSSM._debug(" - no more follows") # send a final ack - segack = SegmentAckPDU( 0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 0, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.request(segack) self.set_state(COMPLETED) @@ -620,15 +667,15 @@ def segmented_confirmation(self, apdu): 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.restart_timer(self.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) + self.restart_timer(self.segmentTimeout) def segmented_confirmation_timeout(self): if _debug: ClientSSM._debug("segmented_confirmation_timeout") @@ -643,9 +690,14 @@ def segmented_confirmation_timeout(self): @bacpypes_debugging class ServerSSM(SSM): - def __init__(self, sap, remoteDevice): - if _debug: ServerSSM._debug("__init__ %s %r", sap, remoteDevice) - SSM.__init__(self, sap, remoteDevice) + def __init__(self, sap, pdu_address): + if _debug: ServerSSM._debug("__init__ %s %r", sap, pdu_address) + SSM.__init__(self, sap, pdu_address) + + # acquire the device info + if self.device_info: + if _debug: ServerSSM._debug(" - acquire device information") + self.ssmSAP.deviceInfoCache.acquire(self.device_info) def set_state(self, newState, timer=0): """This function is called when the client wants to change state.""" @@ -659,8 +711,10 @@ def set_state(self, newState, timer=0): 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) + # release the device info + if self.device_info: + if _debug: ClientSSM._debug(" - release device information") + self.ssmSAP.deviceInfoCache.release(self.device_info) def request(self, apdu): """This function is called by transaction functions to send @@ -668,7 +722,7 @@ def request(self, apdu): if _debug: ServerSSM._debug("request %r", apdu) # make sure it has a good source and destination - apdu.pduSource = self.remoteDevice.address + apdu.pduSource = self.pdu_address apdu.pduDestination = None # send it via the device @@ -697,7 +751,7 @@ def response(self, apdu): # make sure it has a good source and destination apdu.pduSource = None - apdu.pduDestination = self.remoteDevice.address + apdu.pduDestination = self.pdu_address # send it via the device self.ssmSAP.request(apdu) @@ -739,14 +793,15 @@ def confirmation(self, apdu): # 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) + # the segment size is the minimum of the size of the largest packet + # that can be delivered to the client and the largest it can accept + if (not self.device_info) or (self.device_info.maxNpduLength is None): + self.segmentSize = self.maxApduLengthAccepted + else: + self.segmentSize = min(self.device_info.maxNpduLength, self.maxApduLengthAccepted) if _debug: ServerSSM._debug(" - segment size: %r", self.segmentSize) - # compute the segment count ### minus the header? + # compute the segment count if not apdu.pduData: # always at least one segment self.segmentCount = 1 @@ -762,26 +817,31 @@ def confirmation(self, apdu): 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 self.segmentationSupported not in ('segmentedTransmit', 'segmentedBoth'): if _debug: ServerSSM._debug(" - server can't send segmented responses") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return # make sure client supports segmented receive - if self.remoteDevice.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if self.device_info.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? + # make sure we dont exceed the number of segments in our response + # that the device said it was willing to accept in the request + if self.segmentCount > self.maxSegmentsAccepted: + if _debug: ServerSSM._debug(" - client can't receive enough segments") + abort = self.abort(AbortReason.apduTooLong) + self.response(abort) + return # initialize the state self.segmentRetryCount = 0 self.initialSequenceNumber = 0 - self.proposedWindowSize = self.ssmSAP.maxSegmentsAccepted - self.actualWindowSize = 1 + self.actualWindowSize = None # send out the first segment (or the whole thing) if self.segmentCount == 1: @@ -789,7 +849,7 @@ def confirmation(self, apdu): self.set_state(COMPLETED) else: self.response(self.get_segment(0)) - self.set_state(SEGMENTED_RESPONSE, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_RESPONSE, self.segmentTimeout) else: raise RuntimeError("invalid APDU (4)") @@ -837,38 +897,48 @@ def idle(self, apdu): 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 not self.device_info: + if _debug: ServerSSM._debug(" - no client device info") + + elif self.device_info.segmentationSupported == 'noSegmentation': if _debug: ServerSSM._debug(" - client actually supports segmented receive") - self.remoteDevice.segmentationSupported = 'segmentedReceive' + self.device_info.segmentationSupported = 'segmentedReceive' if _debug: ServerSSM._debug(" - tell the cache the info has been updated") - self.ssmSAP.deviceInfoCache.update_device_info(self.remoteDevice) + self.ssmSAP.deviceInfoCache.update_device_info(self.device_info) - elif self.remoteDevice.segmentationSupported == 'segmentedTransmit': + elif self.device_info.segmentationSupported == 'segmentedTransmit': if _debug: ServerSSM._debug(" - client actually supports both segmented transmit and receive") - self.remoteDevice.segmentationSupported = 'segmentedBoth' + self.device_info.segmentationSupported = 'segmentedBoth' if _debug: ServerSSM._debug(" - tell the cache the info has been updated") - self.ssmSAP.deviceInfoCache.update_device_info(self.remoteDevice) + self.ssmSAP.deviceInfoCache.update_device_info(self.device_info) - elif self.remoteDevice.segmentationSupported == 'segmentedReceive': + elif self.device_info.segmentationSupported == 'segmentedReceive': pass - elif self.remoteDevice.segmentationSupported == 'segmentedBoth': + elif self.device_info.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?") + # decode the maximum that the client can receive in one APDU, and if + # there is a value in the device information then use that one because + # it came from reading device object property value or from an I-Am + # message that was received + self.maxApduLengthAccepted = decode_max_apdu_length_accepted(apdu.apduMaxResp) + if self.device_info and self.device_info.maxApduLengthAccepted is not None: + if self.device_info.maxApduLengthAccepted < self.maxApduLengthAccepted: + if _debug: ServerSSM._debug(" - apduMaxResp encoding error") + else: + self.maxApduLengthAccepted = self.device_info.maxApduLengthAccepted + if _debug: ServerSSM._debug(" - maxApduLengthAccepted: %r", self.maxApduLengthAccepted) - # save the number of segments the client is willing to accept in the ack - self.maxSegmentsAccepted = apdu.apduMaxSegs + # save the number of segments the client is willing to accept in the ack, + # if this is None then the value is unknown or more than 64 + self.maxSegmentsAccepted = decode_max_segments_accepted(apdu.apduMaxSegs) # unsegmented request if not apdu.apduSeg: @@ -877,7 +947,7 @@ def idle(self, apdu): return # make sure we support segmented requests - if self.ssmSAP.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if self.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return @@ -885,17 +955,18 @@ def idle(self, apdu): # 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) + # the window size is the minimum of what I would propose and what the + # device has proposed + self.actualWindowSize = min(apdu.apduWin, self.proposedWindowSize) + if _debug: ServerSSM._debug(" - actualWindowSize? min(%r, %r) -> %r", apdu.apduWin, self.proposedWindowSize, self.actualWindowSize) # initialize the state self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 - self.set_state(SEGMENTED_REQUEST, self.ssmSAP.segmentTimeout) + self.set_state(SEGMENTED_REQUEST, self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) if _debug: ServerSSM._debug(" - segAck: %r", segack) self.response(segack) @@ -928,10 +999,10 @@ def segmented_request(self, apdu): 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) + self.restart_timer(self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 1, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(1, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize) self.response(segack) return @@ -947,7 +1018,7 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - no more follows") # send back a final segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.lastSequenceNumber, self.actualWindowSize ) + segack = SegmentAckPDU(0, 1, self.invokeID, self.lastSequenceNumber, self.actualWindowSize) self.response(segack) # forward the whole thing to the application @@ -958,17 +1029,17 @@ def segmented_request(self, apdu): if _debug: ServerSSM._debug(" - last segment in the group") self.initialSequenceNumber = self.lastSequenceNumber - self.restart_timer(self.ssmSAP.segmentTimeout) + self.restart_timer(self.segmentTimeout) # send back a segment ack - segack = SegmentAckPDU( 0, 1, self.invokeID, self.initialSequenceNumber, self.actualWindowSize ) + 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) + self.restart_timer(self.segmentTimeout) def segmented_request_timeout(self): if _debug: ServerSSM._debug("segmented_request_timeout") @@ -1008,10 +1079,13 @@ def segmented_response(self, apdu): if (apdu.apduType == SegmentAckPDU.pduType): if _debug: ServerSSM._debug(" - segment ack") + # actual window size is provided by client + self.actualWindowSize = apdu.apduWin + # 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) + self.restart_timer(self.segmentTimeout) # final ack received? elif self.sentAllSegments: @@ -1024,8 +1098,8 @@ def segmented_response(self, apdu): self.initialSequenceNumber = (apdu.apduSeq + 1) % 256 self.actualWindowSize = apdu.apduWin self.segmentRetryCount = 0 - self.FillWindow(self.initialSequenceNumber) - self.restart_timer(self.ssmSAP.segmentTimeout) + self.fill_window(self.initialSequenceNumber) + self.restart_timer(self.segmentTimeout) # some kind of problem elif (apdu.apduType == AbortPDU.pduType): @@ -1039,10 +1113,10 @@ def segmented_response_timeout(self): if _debug: ServerSSM._debug("segmented_response_timeout") # try again - if self.segmentRetryCount < self.ssmSAP.retryCount: + if self.segmentRetryCount < self.numberOfApduRetries: self.segmentRetryCount += 1 - self.start_timer(self.ssmSAP.segmentTimeout) - self.FillWindow(self.initialSequenceNumber) + self.start_timer(self.segmentTimeout) + self.fill_window(self.initialSequenceNumber) else: # give up self.set_state(ABORTED) @@ -1062,6 +1136,7 @@ def __init__(self, localDevice=None, deviceInfoCache=None, sap=None, cid=None): ServiceAccessPoint.__init__(self, sap) # save a reference to the device information cache + self.localDevice = localDevice self.deviceInfoCache = deviceInfoCache # client settings @@ -1072,27 +1147,19 @@ def __init__(self, localDevice=None, deviceInfoCache=None, sap=None, cid=None): self.serverTransactions = [] # confirmed request defaults - self.retryCount = 3 - self.retryTimeout = 3000 + self.numberOfApduRetries = 3 + self.apduTimeout = 3000 self.maxApduLengthAccepted = 1024 # segmentation defaults self.segmentationSupported = 'noSegmentation' self.segmentTimeout = 1500 - self.maxSegmentsAccepted = 8 + self.maxSegmentsAccepted = 2 + self.proposedWindowSize = 2 # 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 @@ -1111,7 +1178,7 @@ def get_next_invoke_id(self, addr): raise RuntimeError("no available invoke ID") for tr in self.clientTransactions: - if (invokeID == tr.invokeID) and (addr == tr.remoteDevice.address): + if (invokeID == tr.invokeID) and (addr == tr.pdu_address): break else: break @@ -1152,14 +1219,11 @@ def confirmation(self, pdu): 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): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: - # find the remote device information - remoteDevice = self.deviceInfoCache.get_device_info(apdu.pduSource) - # build a server transaction - tr = ServerSSM(self, remoteDevice) + tr = ServerSSM(self, apdu.pduSource) # add it to our transactions to track it self.serverTransactions.append(tr) @@ -1178,7 +1242,7 @@ def confirmation(self, pdu): # find the client transaction this is acking for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1190,7 +1254,7 @@ def confirmation(self, pdu): # find the transaction being aborted if apdu.apduSrv: for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1199,7 +1263,7 @@ def confirmation(self, pdu): tr.confirmation(apdu) else: for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1211,7 +1275,7 @@ def confirmation(self, pdu): # find the transaction being aborted if apdu.apduSrv: for tr in self.clientTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1220,7 +1284,7 @@ def confirmation(self, pdu): tr.confirmation(apdu) else: for tr in self.serverTransactions: - if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.remoteDevice.address): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduSource == tr.pdu_address): break else: return @@ -1264,19 +1328,15 @@ def sap_indication(self, apdu): 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): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.pdu_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) + tr = ClientSSM(self, apdu.pduDestination) if _debug: StateMachineAccessPoint._debug(" - client segmentation state machine: %r", tr) # add it to our transactions to track it @@ -1300,7 +1360,7 @@ def sap_confirmation(self, apdu): 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): + if (apdu.apduInvokeID == tr.invokeID) and (apdu.pduDestination == tr.pdu_address): break else: return @@ -1327,23 +1387,26 @@ def indication(self, apdu): if _debug: ApplicationServiceAccessPoint._debug("indication %r", apdu) if isinstance(apdu, ConfirmedRequestPDU): + # assume no errors found + error_found = None + + # look up the class associated with the service 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 + error_found = UnrecognizedService() - try: - xpdu = atype() - xpdu.decode(apdu) - except RejectException as err: - ApplicationServiceAccessPoint._debug(" - decoding reject: %r", err) - error_found = err - except AbortException as err: - ApplicationServiceAccessPoint._debug(" - decoding abort: %r", err) - error_found = err + # no error so far, keep going + if not error_found: + try: + xpdu = atype() + xpdu.decode(apdu) + except RejectException as err: + ApplicationServiceAccessPoint._debug(" - decoding reject: %r", err) + error_found = err + except AbortException as err: + ApplicationServiceAccessPoint._debug(" - decoding abort: %r", err) + error_found = err # no error so far, keep going if not error_found: diff --git a/py34/bacpypes/bvllservice.py b/py34/bacpypes/bvllservice.py index dddb2d01..577deb72 100755 --- a/py34/bacpypes/bvllservice.py +++ b/py34/bacpypes/bvllservice.py @@ -541,33 +541,38 @@ def confirmation(self, pdu): return - elif isinstance(pdu, OriginalUnicastNPDU): + if isinstance(pdu, OriginalUnicastNPDU): # build a vanilla PDU xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) # send it upstream self.response(xpdu) - # check the BBMD registration status, we may not be registered - if self.registrationStatus != 0: - if _debug: BIPForeign._debug(" - packet dropped, unregistered") - return - - if isinstance(pdu, ReadBroadcastDistributionTableAck): - # send this to the service access point - self.sap_response(pdu) + elif isinstance(pdu, ForwardedNPDU): + # check the BBMD registration status, we may not be registered + if self.registrationStatus != 0: + if _debug: BIPForeign._debug(" - packet dropped, unregistered") + return - elif isinstance(pdu, ReadForeignDeviceTableAck): - # send this to the service access point - self.sap_response(pdu) + # make sure the forwarded PDU from the bbmd + if pdu.pduSource != self.bbmdAddress: + if _debug: BIPForeign._debug(" - packet dropped, not from the BBMD") + return - elif isinstance(pdu, ForwardedNPDU): # build a PDU with the source from the real source xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) # send it upstream self.response(xpdu) + elif isinstance(pdu, ReadBroadcastDistributionTableAck): + # send this to the service access point + self.sap_response(pdu) + + elif isinstance(pdu, ReadForeignDeviceTableAck): + # send this to the service access point + self.sap_response(pdu) + elif isinstance(pdu, WriteBroadcastDistributionTable): # build a response xpdu = Result(code=0x0010, user_data=pdu.pduUserData) @@ -616,6 +621,9 @@ def confirmation(self, pdu): # send it downstream self.request(xpdu) + elif isinstance(pdu, OriginalBroadcastNPDU): + if _debug: BIPForeign._debug(" - packet dropped") + else: BIPForeign._warning("invalid pdu type: %s", type(pdu)) @@ -690,7 +698,7 @@ def indication(self, pdu): # make an original unicast PDU xpdu = OriginalUnicastNPDU(pdu, user_data=pdu.pduUserData) xpdu.pduDestination = pdu.pduDestination - if _debug: BIPBBMD._debug(" - xpdu: %r", xpdu) + if _debug: BIPBBMD._debug(" - original unicast xpdu: %r", xpdu) # send it downstream self.request(xpdu) @@ -713,13 +721,13 @@ def indication(self, pdu): for bdte in self.bbmdBDT: if bdte != self.bbmdAddress: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) else: @@ -735,8 +743,9 @@ def confirmation(self, pdu): elif isinstance(pdu, WriteBroadcastDistributionTable): # build a response - xpdu = Result(code=99, user_data=pdu.pduUserData) + xpdu = Result(code=0x0010, user_data=pdu.pduUserData) xpdu.pduDestination = pdu.pduSource + if _debug: BIPBBMD._debug(" - xpdu: %r", xpdu) # send it downstream self.request(xpdu) @@ -755,27 +764,38 @@ def confirmation(self, pdu): self.sap_response(pdu) elif isinstance(pdu, ForwardedNPDU): - # build a PDU with the source from the real source - xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.bvlciAddress, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # build a forwarded NPDU to send out xpdu = ForwardedNPDU(pdu.bvlciAddress, pdu, destination=None, user_data=pdu.pduUserData) if _debug: BIPBBMD._debug(" - forwarded xpdu: %r", xpdu) - # look for self as first entry in the BDT - if self.bbmdBDT and (self.bbmdBDT[0] == self.bbmdAddress): - xpdu.pduDestination = LocalBroadcast() - if _debug: BIPBBMD._debug(" - local broadcast") - self.request(xpdu) + # if this was unicast to us, do next hop + if pdu.pduDestination.addrType == Address.localStationAddr: + if _debug: BIPBBMD._debug(" - unicast message") + + # if this BBMD is listed in its BDT, send a local broadcast + if self.bbmdAddress in self.bbmdBDT: + xpdu.pduDestination = LocalBroadcast() + if _debug: BIPBBMD._debug(" - local broadcast") + self.request(xpdu) + + elif pdu.pduDestination.addrType == Address.localBroadcastAddr: + if _debug: BIPBBMD._debug(" - directed broadcast message") + + else: + BIPBBMD._warning("invalid destination address: %r", pdu.pduDestination) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) elif isinstance(pdu, RegisterForeignDevice): @@ -814,12 +834,13 @@ def confirmation(self, pdu): self.request(xpdu) elif isinstance(pdu, DistributeBroadcastToNetwork): - # build a PDU with a local broadcast address - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # build a forwarded NPDU to send out xpdu = ForwardedNPDU(pdu.pduSource, pdu, user_data=pdu.pduUserData) @@ -829,35 +850,37 @@ def confirmation(self, pdu): for bdte in self.bbmdBDT: if bdte == self.bbmdAddress: xpdu.pduDestination = LocalBroadcast() - if _debug: BIPBBMD._debug(" - local broadcast") + if _debug: BIPBBMD._debug(" - local broadcast") self.request(xpdu) else: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the other registered foreign devices for fdte in self.bbmdFDT: if fdte.fdAddress != pdu.pduSource: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) elif isinstance(pdu, OriginalUnicastNPDU): - # build a vanilla PDU - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=pdu.pduDestination, user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) elif isinstance(pdu, OriginalBroadcastNPDU): - # build a PDU with a local broadcast address - xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) - if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) + # send it upstream if there is a network layer + if self.serverPeer: + # build a PDU with a local broadcast address + xpdu = PDU(pdu.pduData, source=pdu.pduSource, destination=LocalBroadcast(), user_data=pdu.pduUserData) + if _debug: BIPBBMD._debug(" - upstream xpdu: %r", xpdu) - # send it upstream - self.response(xpdu) + self.response(xpdu) # make a forwarded PDU xpdu = ForwardedNPDU(pdu.pduSource, pdu, user_data=pdu.pduUserData) @@ -867,13 +890,13 @@ def confirmation(self, pdu): for bdte in self.bbmdBDT: if bdte != self.bbmdAddress: xpdu.pduDestination = Address( ((bdte.addrIP|~bdte.addrMask), bdte.addrPort) ) - if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to peer: %r", xpdu.pduDestination) self.request(xpdu) # send it to the registered foreign devices for fdte in self.bbmdFDT: xpdu.pduDestination = fdte.fdAddress - if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) + if _debug: BIPBBMD._debug(" - sending to foreign device: %r", xpdu.pduDestination) self.request(xpdu) else: @@ -923,7 +946,7 @@ def delete_foreign_device_table_entry(self, addr): del self.bbmdFDT[i] break else: - stat = 99 ### entry not found + stat = 0x0050 ### entry not found # return status return stat @@ -950,10 +973,6 @@ def add_peer(self, addr): else: raise TypeError("addr must be a string or an Address") - # if it's this BBMD, make it the first one - if self.bbmdBDT and (addr == self.bbmdAddress): - raise RuntimeError("add self to BDT as first address") - # see if it's already there for bdte in self.bbmdBDT: if addr == bdte: diff --git a/py34/bacpypes/local/device.py b/py34/bacpypes/local/device.py index c823b897..88d7ba99 100644 --- a/py34/bacpypes/local/device.py +++ b/py34/bacpypes/local/device.py @@ -2,7 +2,9 @@ from ..debugging import bacpypes_debugging, ModuleLogger -from ..primitivedata import Date, Time, ObjectIdentifier +from ..primitivedata import Null, Boolean, Unsigned, Integer, Real, Double, \ + OctetString, CharacterString, BitString, Enumerated, Date, Time, \ + ObjectIdentifier from ..constructeddata import ArrayOf from ..basetypes import ServicesSupported @@ -107,21 +109,113 @@ class LocalDeviceObject(CurrentPropertyListMixIn, DeviceObject): def __init__(self, **kwargs): if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) - # fill in default property values not in kwargs - for attr, value in LocalDeviceObject.defaultProperties.items(): - if attr not in kwargs: - kwargs[attr] = value - - for key, value in kwargs.items(): - if key.startswith("_"): - setattr(self, key, value) - del kwargs[key] + # start with an empty dictionary of device object properties + init_args = {} + ini_arg = kwargs.get('ini', None) + if _debug: LocalDeviceObject._debug(" - ini_arg: %r", dir(ini_arg)) - # check for registration + # check for registration as a keyword parameter or in the INI file if self.__class__ not in registered_object_types.values(): - if 'vendorIdentifier' not in kwargs: + if _debug: LocalDeviceObject._debug(" - unregistered") + + vendor_identifier = kwargs.get('vendorIdentifier', None) + if _debug: LocalDeviceObject._debug(" - keyword vendor identifier: %r", vendor_identifier) + + if vendor_identifier is None: + vendor_identifier = getattr(ini_arg, 'vendoridentifier', None) + if _debug: LocalDeviceObject._debug(" - INI vendor identifier: %r", vendor_identifier) + + if vendor_identifier is None: raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") - register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) + + register_object_type(self.__class__, vendor_id=vendor_identifier) + + # look for properties, fill in values from the keyword arguments or + # the INI parameter (converted to a proper value) if it was provided + for propid, prop in self._properties.items(): + # special processing for object identifier + if propid == 'objectIdentifier': + continue + + # use keyword argument if it was provided + if propid in kwargs: + prop_value = kwargs[propid] + else: + prop_value = getattr(ini_arg, propid.lower(), None) + if prop_value is None: + continue + + prop_datatype = prop.datatype + + if issubclass(prop_datatype, Null): + if prop_value != "Null": + raise ValueError("invalid null property value: %r" % (propid,)) + prop_value = None + + elif issubclass(prop_datatype, Boolean): + prop_value = prop_value.lower() + if prop_value not in ('true', 'false', 'set', 'reset'): + raise ValueError("invalid boolean property value: %r" % (propid,)) + prop_value = prop_value in ('true', 'set') + + elif issubclass(prop_datatype, (Unsigned, Integer)): + try: + prop_value = int(prop_value) + except ValueError: + raise ValueError("invalid unsigned or integer property value: %r" % (propid,)) + + elif issubclass(prop_datatype, (Real, Double)): + try: + prop_value = float(prop_value) + except ValueError: + raise ValueError("invalid real or double property value: %r" % (propid,)) + + elif issubclass(prop_datatype, OctetString): + try: + prop_value = xtob(prop_value) + except: + raise ValueError("invalid octet string property value: %r" % (propid,)) + + elif issubclass(prop_datatype, CharacterString): + pass + + elif issubclass(prop_datatype, BitString): + try: + bstr, prop_value = prop_value, [] + for b in bstr: + if b not in ('0', '1'): + raise ValueError + prop_value.append(int(b)) + except: + raise ValueError("invalid bit string property value: %r" % (propid,)) + + elif issubclass(prop_datatype, Enumerated): + pass + + else: + raise ValueError("cannot interpret %r INI paramter" % (propid,)) + if _debug: LocalDeviceObject._debug(" - property %r: %r", propid, prop_value) + + # at long last + init_args[propid] = prop_value + + # check for object identifier as a keyword parameter or in the INI file, + # and it might be just an int, so make it a tuple if necessary + if 'objectIdentifier' in kwargs: + object_identifier = kwargs['objectIdentifier'] + if isinstance(object_identifier, int): + object_identifier = ('device', object_identifier) + elif hasattr(ini_arg, 'objectidentifier'): + object_identifier = ('device', int(getattr(ini_arg, 'objectidentifier'))) + else: + raise RuntimeError("objectIdentifier is required") + init_args['objectIdentifier'] = object_identifier + if _debug: LocalDeviceObject._debug(" - object identifier: %r", object_identifier) + + # fill in default property values not in init_args + for attr, value in LocalDeviceObject.defaultProperties.items(): + if attr not in init_args: + init_args[attr] = value # check for properties this class implements if 'localDate' in kwargs: @@ -131,27 +225,23 @@ def __init__(self, **kwargs): if 'protocolServicesSupported' in kwargs: raise RuntimeError("protocolServicesSupported is provided by LocalDeviceObject and cannot be overridden") - # the object identifier is required for the object list - if 'objectIdentifier' not in kwargs: - raise RuntimeError("objectIdentifier is required") - - # coerce the object identifier - object_identifier = kwargs['objectIdentifier'] - if isinstance(object_identifier, int): - object_identifier = ('device', object_identifier) - # the object list is provided if 'objectList' in kwargs: raise RuntimeError("objectList is provided by LocalDeviceObject and cannot be overridden") - kwargs['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) + init_args['objectList'] = ArrayOf(ObjectIdentifier)([object_identifier]) # check for a minimum value - if kwargs['maxApduLengthAccepted'] < 50: + if init_args['maxApduLengthAccepted'] < 50: raise ValueError("invalid max APDU length accepted") # dump the updated attributes - if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) + if _debug: LocalDeviceObject._debug(" - init_args: %r", init_args) # proceed as usual - super(LocalDeviceObject, self).__init__(**kwargs) + super(LocalDeviceObject, self).__init__(**init_args) + + # pass along special property values that are not BACnet properties + for key, value in kwargs.items(): + if key.startswith("_"): + setattr(self, key, value) diff --git a/py34/bacpypes/object.py b/py34/bacpypes/object.py index 97b58992..9f2a4e99 100755 --- a/py34/bacpypes/object.py +++ b/py34/bacpypes/object.py @@ -1422,7 +1422,7 @@ class EventEnrollmentObject(Object): , ReadableProperty('eventTimeStamps', ArrayOf(TimeStamp)) , OptionalProperty('eventMessageTexts', ArrayOf(CharacterString)) , OptionalProperty('eventMessageTextsConfig', ArrayOf(CharacterString)) - , OptionalProperty('eventDetectionEnable', Boolean) + , ReadableProperty('eventDetectionEnable', Boolean) , OptionalProperty('eventAlgorithmInhibitRef', ObjectPropertyReference) , OptionalProperty('eventAlgorithmInhibit', Boolean) , OptionalProperty('timeDelayNormal', Unsigned) diff --git a/py34/bacpypes/task.py b/py34/bacpypes/task.py index 1c89a4f7..0c25c33a 100755 --- a/py34/bacpypes/task.py +++ b/py34/bacpypes/task.py @@ -8,6 +8,7 @@ from time import time as _time from heapq import heapify, heappush, heappop +import itertools from .singleton import SingletonLogging from .debugging import DebugContents, Logging, ModuleLogger, bacpypes_debugging @@ -276,6 +277,9 @@ def __init__(self): # task manager is this instance _task_manager = self + # unique sequence counter for tasks scheduled at the same time + self.counter = itertools.count() + # there may be tasks created that couldn't be scheduled # because a task manager wasn't created yet. if _unscheduled_tasks: @@ -300,7 +304,7 @@ def install_task(self, task): self.suspend_task(task) # save this in the task list - heappush( self.tasks, (task.taskTime, task) ) + heappush( self.tasks, (task.taskTime, next(self.counter), task) ) if _debug: TaskManager._debug(" - tasks: %r", self.tasks) task.isScheduled = True @@ -313,7 +317,7 @@ def suspend_task(self, task): if _debug: TaskManager._debug("suspend_task %r", task) # remove this guy - for i, (when, curtask) in enumerate(self.tasks): + for i, (when, n, curtask) in enumerate(self.tasks): if task is curtask: if _debug: TaskManager._debug(" - task found") del self.tasks[i] @@ -348,7 +352,7 @@ def get_next_task(self): if self.tasks: # look at the first task - when, nxttask = self.tasks[0] + when, n, nxttask = self.tasks[0] if when <= now: # pull it off the list and mark that it's no longer scheduled heappop(self.tasks) @@ -356,7 +360,7 @@ def get_next_task(self): task.isScheduled = False if self.tasks: - when, nxttask = self.tasks[0] + when, n, nxttask = self.tasks[0] # peek at the next task, return how long to wait delta = max(when - now, 0.0) else: diff --git a/py34/bacpypes/vlan.py b/py34/bacpypes/vlan.py index ead2d03a..5908e898 100755 --- a/py34/bacpypes/vlan.py +++ b/py34/bacpypes/vlan.py @@ -32,6 +32,7 @@ def __init__(self, name='', broadcast_address=None, drop_percent=0.0): self.name = name self.nodes = [] + self.broadcast_address = broadcast_address self.drop_percent = drop_percent @@ -161,9 +162,9 @@ class IPNetwork(Network): ('1.2.3.255', 5) and the other nodes must have the same tuple. """ - def __init__(self): + def __init__(self, name=''): if _debug: IPNetwork._debug("__init__") - Network.__init__(self) + Network.__init__(self, name=name) def add_node(self, node): if _debug: IPNetwork._debug("add_node %r", node) @@ -213,11 +214,12 @@ def __init__(self, addr, lan=None, promiscuous=False, spoofing=False, sid=None): @bacpypes_debugging class IPRouterNode(Client): - def __init__(self, router, addr, lan=None): + def __init__(self, router, addr, lan): if _debug: IPRouterNode._debug("__init__ %r %r lan=%r", router, addr, lan) - # save the reference to the router + # save the references to the router for packets and the lan for debugging self.router = router + self.lan = lan # make ourselves an IPNode and bind to it self.node = IPNode(addr, lan=lan, promiscuous=True, spoofing=True) @@ -238,6 +240,10 @@ def process_pdu(self, pdu): # pass it downstream self.request(pdu) + def __repr__(self): + return "<%s for %s>" % (self.__class__.__name__, self.lan.name) + + # # IPRouter # diff --git a/samples/AccumulatorObject.py b/samples/AccumulatorObject.py index 01130394..d317d418 100755 --- a/samples/AccumulatorObject.py +++ b/samples/AccumulatorObject.py @@ -70,13 +70,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=('device', int(args.ini.objectidentifier)), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/COVClient.py b/samples/COVClient.py index e79ba964..dd6f8aa0 100755 --- a/samples/COVClient.py +++ b/samples/COVClient.py @@ -217,13 +217,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = SubscribeCOVApplication(this_device, args.ini.address) diff --git a/samples/COVClientApp.py b/samples/COVClientApp.py new file mode 100755 index 00000000..41c601f0 --- /dev/null +++ b/samples/COVClientApp.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python + +""" +Configured with a subscription context object which is passed to the +application, it sends a SubscribeCOVRequest and listens for confirmed or +unconfirmed COV notifications, lines them up with the context, and passes the +APDU to the context to print out. + +Making multiple subscription contexts and keeping them active based on their +lifetime is left as an exercise for the reader. +""" + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser + +from bacpypes.core import run, deferred +from bacpypes.iocb import IOCB + +from bacpypes.pdu import Address +from bacpypes.apdu import SubscribeCOVRequest, SimpleAckPDU +from bacpypes.errors import ExecutionError + +from bacpypes.app import BIPSimpleApplication +from bacpypes.local.device import LocalDeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +this_application = None + +subscription_contexts = {} +next_proc_id = 1 + +# +# SubscriptionContext +# + +@bacpypes_debugging +class SubscriptionContext: + + def __init__(self, address, objid, confirmed=None, lifetime=None): + if _debug: SubscriptionContext._debug("__init__ %r %r confirmed=%r lifetime=%r", address, objid, confirmed, lifetime) + global subscription_contexts, next_proc_id + + # destination for subscription requests + self.address = address + + # assign a unique process identifer and keep track of it + self.subscriberProcessIdentifier = next_proc_id + next_proc_id += 1 + subscription_contexts[self.subscriberProcessIdentifier] = self + + self.monitoredObjectIdentifier = objid + self.issueConfirmedNotifications = confirmed + self.lifetime = lifetime + + def cov_notification(self, apdu): + if _debug: SubscriptionContext._debug("cov_notification %r", apdu) + + # make a rash assumption that the property value is going to be + # a single application encoded tag + print("{} {} changed\n {}".format( + apdu.pduSource, + apdu.monitoredObjectIdentifier, + ",\n ".join("{} = {}".format( + element.propertyIdentifier, + str(element.value.tagList[0].app_to_object().value), + ) for element in apdu.listOfValues), + )) + +# +# SubscribeCOVApplication +# + +@bacpypes_debugging +class SubscribeCOVApplication(BIPSimpleApplication): + + def __init__(self, *args): + if _debug: SubscribeCOVApplication._debug("__init__ %r", args) + BIPSimpleApplication.__init__(self, *args) + + def send_subscription(self, context): + if _debug: SubscribeCOVApplication._debug("send_subscription %r", context) + + # build a request + request = SubscribeCOVRequest( + subscriberProcessIdentifier=context.subscriberProcessIdentifier, + monitoredObjectIdentifier=context.monitoredObjectIdentifier, + ) + request.pduDestination = context.address + + # optional parameters + if context.issueConfirmedNotifications is not None: + request.issueConfirmedNotifications = context.issueConfirmedNotifications + if context.lifetime is not None: + request.lifetime = context.lifetime + + # make an IOCB + iocb = IOCB(request) + if _debug: SubscribeCOVApplication._debug(" - iocb: %r", iocb) + + # callback when it is acknowledged + iocb.add_callback(self.subscription_acknowledged) + + # give it to the application + this_application.request_io(iocb) + + def subscription_acknowledged(self, iocb): + if _debug: SubscribeCOVApplication._debug("subscription_acknowledged %r", iocb) + + # do something for success + if iocb.ioResponse: + if _debug: SubscribeCOVApplication._debug(" - response: %r", iocb.ioResponse) + + # do something for error/reject/abort + if iocb.ioError: + if _debug: SubscribeCOVApplication._debug(" - error: %r", iocb.ioError) + + def do_ConfirmedCOVNotificationRequest(self, apdu): + if _debug: SubscribeCOVApplication._debug("do_ConfirmedCOVNotificationRequest %r", apdu) + + # look up the process identifier + context = subscription_contexts.get(apdu.subscriberProcessIdentifier, None) + if not context or apdu.pduSource != context.address: + if _debug: SubscribeCOVApplication._debug(" - no context") + + # this is turned into an ErrorPDU and sent back to the client + raise ExecutionError('services', 'unknownSubscription') + + # now tell the context object + context.cov_notification(apdu) + + # success + response = SimpleAckPDU(context=apdu) + if _debug: SubscribeCOVApplication._debug(" - simple_ack: %r", response) + + # return the result + self.response(response) + + def do_UnconfirmedCOVNotificationRequest(self, apdu): + if _debug: SubscribeCOVApplication._debug("do_UnconfirmedCOVNotificationRequest %r", apdu) + + # look up the process identifier + context = subscription_contexts.get(apdu.subscriberProcessIdentifier, None) + if not context or apdu.pduSource != context.address: + if _debug: SubscribeCOVApplication._debug(" - no context") + return + + # now tell the context object + context.cov_notification(apdu) + +# +# __main__ +# + +def main(): + global this_application + + # parse the command line arguments + args = ConfigArgumentParser(description=__doc__).parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # make a device object + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) + + # make a simple application + this_application = SubscribeCOVApplication(this_device, args.ini.address) + + # make a subscription context + context = SubscriptionContext(Address("10.0.1.31"), ('analogValue', 1), False, 60) + + # send the subscription when the stack is ready + deferred(this_application.send_subscription, context) + + _log.debug("running") + + run() + + _log.debug("fini") + +if __name__ == "__main__": + main() diff --git a/samples/COVServer.py b/samples/COVServer.py index ad2558a4..4c32dcda 100755 --- a/samples/COVServer.py +++ b/samples/COVServer.py @@ -365,16 +365,11 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - test_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application - test_application = SubscribeCOVApplication(test_device, args.ini.address) + test_application = SubscribeCOVApplication(this_device, args.ini.address) # make an analog value object test_av = AnalogValueObject( @@ -388,7 +383,7 @@ def main(): # add it to the device test_application.add_object(test_av) - _log.debug(" - object list: %r", test_device.objectList) + _log.debug(" - object list: %r", this_device.objectList) # make a binary value object test_bv = BinaryValueObject( @@ -402,13 +397,6 @@ def main(): # add it to the device test_application.add_object(test_bv) - # get the services supported - services_supported = test_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - test_device.protocolServicesSupported = services_supported.value - # make a console if args.console: test_console = COVConsoleCmd() diff --git a/samples/CommandableMixin.py b/samples/CommandableMixin.py index 41b8998c..2a268e09 100644 --- a/samples/CommandableMixin.py +++ b/samples/CommandableMixin.py @@ -379,13 +379,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/DeviceCommunicationControl.py b/samples/DeviceCommunicationControl.py index e4ce6d63..d679a097 100755 --- a/samples/DeviceCommunicationControl.py +++ b/samples/DeviceCommunicationControl.py @@ -123,13 +123,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/DeviceDiscovery.py b/samples/DeviceDiscovery.py index e0f7276b..11b0c1af 100755 --- a/samples/DeviceDiscovery.py +++ b/samples/DeviceDiscovery.py @@ -16,10 +16,9 @@ from bacpypes.iocb import IOCB from bacpypes.pdu import Address, GlobalBroadcast -from bacpypes.apdu import WhoIsRequest, IAmRequest, ReadPropertyRequest, ReadPropertyACK +from bacpypes.apdu import WhoIsRequest, ReadPropertyRequest, ReadPropertyACK from bacpypes.primitivedata import CharacterString -from bacpypes.basetypes import ServicesSupported -from bacpypes.errors import DecodingError +from bacpypes.errors import MissingRequiredParameter from bacpypes.app import BIPSimpleApplication from bacpypes.local.device import LocalDeviceObject @@ -149,7 +148,6 @@ def do_whois(self, args): try: # gather the parameters - request = WhoIsRequest() if (len(args) == 1) or (len(args) == 3): addr = Address(args[0]) del args[0] @@ -200,13 +198,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = DiscoveryApplication(this_device, args.ini.address) diff --git a/samples/DeviceDiscoveryForeign.py b/samples/DeviceDiscoveryForeign.py index 1e7b977e..8634552b 100755 --- a/samples/DeviceDiscoveryForeign.py +++ b/samples/DeviceDiscoveryForeign.py @@ -16,10 +16,9 @@ from bacpypes.iocb import IOCB from bacpypes.pdu import Address, GlobalBroadcast -from bacpypes.apdu import WhoIsRequest, IAmRequest, ReadPropertyRequest, ReadPropertyACK +from bacpypes.apdu import WhoIsRequest, ReadPropertyRequest, ReadPropertyACK from bacpypes.primitivedata import CharacterString -from bacpypes.basetypes import ServicesSupported -from bacpypes.errors import DecodingError +from bacpypes.errors import MissingRequiredParameter from bacpypes.app import BIPForeignApplication from bacpypes.local.device import LocalDeviceObject @@ -149,7 +148,6 @@ def do_whois(self, args): try: # gather the parameters - request = WhoIsRequest() if (len(args) == 1) or (len(args) == 3): addr = Address(args[0]) del args[0] @@ -200,13 +198,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = DiscoveryApplication( diff --git a/samples/HTTPServer.py b/samples/HTTPServer.py index 61d1c21e..64681e63 100755 --- a/samples/HTTPServer.py +++ b/samples/HTTPServer.py @@ -179,13 +179,8 @@ class ThreadedTCPServer(SocketServer.ThreadingMixIn, SocketServer.TCPServer): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/LocalScheduleObject.py b/samples/LocalScheduleObject.py index 20e6c48b..83776c85 100644 --- a/samples/LocalScheduleObject.py +++ b/samples/LocalScheduleObject.py @@ -74,13 +74,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=('device', int(args.ini.objectidentifier)), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/MultiStateValueObject.py b/samples/MultiStateValueObject.py index b3c9b903..cd3d201f 100755 --- a/samples/MultiStateValueObject.py +++ b/samples/MultiStateValueObject.py @@ -33,13 +33,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/MultipleReadProperty.py b/samples/MultipleReadProperty.py index 4bf2dde5..64064683 100755 --- a/samples/MultipleReadProperty.py +++ b/samples/MultipleReadProperty.py @@ -18,7 +18,7 @@ from bacpypes.pdu import Address from bacpypes.object import get_datatype -from bacpypes.apdu import ReadPropertyRequest, Error, AbortPDU, ReadPropertyACK +from bacpypes.apdu import ReadPropertyRequest from bacpypes.primitivedata import Unsigned from bacpypes.constructeddata import Array @@ -131,13 +131,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = ReadPointListApplication(point_list, this_device, args.ini.address) diff --git a/samples/MultipleReadPropertyHammer.py b/samples/MultipleReadPropertyHammer.py index fbac9694..4a641aaa 100755 --- a/samples/MultipleReadPropertyHammer.py +++ b/samples/MultipleReadPropertyHammer.py @@ -201,13 +201,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = ReadPointListApplication(this_device, args.ini.address) diff --git a/samples/MultipleReadPropertyThreaded.py b/samples/MultipleReadPropertyThreaded.py index 652057ae..4ad8c0d7 100755 --- a/samples/MultipleReadPropertyThreaded.py +++ b/samples/MultipleReadPropertyThreaded.py @@ -123,13 +123,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/NATRouter.py b/samples/NATRouter.py new file mode 100644 index 00000000..fa9b70c1 --- /dev/null +++ b/samples/NATRouter.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python + +""" +This sample application presents itself as a router between an "inside" network +that sits behind a NAT and a "global" network of other NAT router peers. + +$ python NATRouter.py addr1 port1 net1 addr2 port2 net2 + + addr1 - local address like 192.168.1.10/24 + port1 - local port + net1 - local network number + addr2 - global address like 201.1.1.1:47809 + port2 - local mapped port + net2 - global network number + +The sample addresses are like running BR1 from Figure J-8, Clause J.7.5. +""" + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ArgumentParser + +from bacpypes.core import run +from bacpypes.comm import bind + +from bacpypes.pdu import Address +from bacpypes.netservice import NetworkServiceAccessPoint, NetworkServiceElement +from bacpypes.bvllservice import BIPBBMD, BIPNAT, AnnexJCodec, UDPMultiplexer + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# NATRouter +# + +@bacpypes_debugging +class NATRouter: + + def __init__(self, addr1, port1, net1, addr2, port2, net2): + if _debug: NATRouter._debug("__init__ %r %r %r %r %r %r", addr1, port1, net1, addr2, port2, net2) + + # a network service access point will be needed + self.nsap = NetworkServiceAccessPoint() + + # give the NSAP a generic network layer service element + self.nse = NetworkServiceElement() + bind(self.nse, self.nsap) + + #== First stack + + # local address + local_addr = Address("{}:{}".format(addr1, port1)) + + # create a BBMD stack + self.s1_bip = BIPBBMD(local_addr) + self.s1_annexj = AnnexJCodec() + self.s1_mux = UDPMultiplexer(local_addr) + + # bind the bottom layers + bind(self.s1_bip, self.s1_annexj, self.s1_mux.annexJ) + + # bind the BIP stack to the local network + self.nsap.bind(self.s1_bip, net1, addr1) + + #== Second stack + + # global address + global_addr = Address(addr2) + nat_addr = Address("{}:{}".format(addr1, port2)) + + # create a NAT stack + self.s2_bip = BIPNAT(global_addr) + self.s2_annexj = AnnexJCodec() + self.s2_mux = UDPMultiplexer(nat_addr) + + # bind the bottom layers + bind(self.s2_bip, self.s2_annexj, self.s2_mux.annexJ) + + # bind the BIP stack to the global network + self.nsap.bind(self.s2_bip, net2) + +# +# __main__ +# + +def main(): + # parse the command line arguments + parser = ArgumentParser(description=__doc__) + + # add an argument for local address + parser.add_argument('addr1', type=str, + help='address of first network', + ) + + # add an argument for local port + parser.add_argument('port1', type=int, + help='port number of local network', + ) + + # add an argument for interval + parser.add_argument('net1', type=int, + help='network number of local network', + ) + + # add an argument for interval + parser.add_argument('addr2', type=str, + help='address of global network (outside NAT)', + ) + + # add an argument for local port + parser.add_argument('port2', type=int, + help='port number of global forwarded port', + ) + + # add an argument for interval + parser.add_argument('net2', type=int, + help='network number of global network', + ) + + # now parse the arguments + args = parser.parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # create the router + router = NATRouter(args.addr1, args.port1, args.net1, args.addr2, args.port2, args.net2) + if _debug: _log.debug(" - router: %r", router) + + _log.debug("running") + + run() + + _log.debug("fini") + + +if __name__ == "__main__": + main() diff --git a/samples/RandomAnalogValueSleep.py b/samples/RandomAnalogValueSleep.py index ef944b25..0f9cf3de 100644 --- a/samples/RandomAnalogValueSleep.py +++ b/samples/RandomAnalogValueSleep.py @@ -108,13 +108,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=('device', int(args.ini.objectidentifier)), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadAllProperties.py b/samples/ReadAllProperties.py index 8cd81627..c80bf373 100755 --- a/samples/ReadAllProperties.py +++ b/samples/ReadAllProperties.py @@ -11,15 +11,12 @@ from bacpypes.debugging import bacpypes_debugging, ModuleLogger from bacpypes.consolelogging import ConfigArgumentParser -from bacpypes.consolecmd import ConsoleCmd from bacpypes.core import run, stop, deferred from bacpypes.iocb import IOCB from bacpypes.pdu import Address -from bacpypes.apdu import ReadPropertyRequest, ReadPropertyACK -from bacpypes.primitivedata import Unsigned -from bacpypes.constructeddata import Array +from bacpypes.apdu import ReadPropertyRequest from bacpypes.app import BIPSimpleApplication from bacpypes.object import get_object_class, get_datatype @@ -157,13 +154,8 @@ def main(): if _debug: _log.debug(" - property_list: %r", property_list) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = ReadPropertyApplication(this_device, args.ini.address) diff --git a/samples/ReadObjectList.py b/samples/ReadObjectList.py new file mode 100755 index 00000000..f51240ac --- /dev/null +++ b/samples/ReadObjectList.py @@ -0,0 +1,239 @@ +#!/usr/bin/env python + +""" +This application is given the device instance number of a device and its +address read the object list, then for each object, read the object name. +""" + +from collections import deque + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser + +from bacpypes.core import run, deferred, stop +from bacpypes.iocb import IOCB + +from bacpypes.primitivedata import ObjectIdentifier, CharacterString +from bacpypes.constructeddata import ArrayOf + +from bacpypes.pdu import Address +from bacpypes.apdu import ReadPropertyRequest, ReadPropertyACK + +from bacpypes.app import BIPSimpleApplication +from bacpypes.local.device import LocalDeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +this_device = None +this_application = None + +# convenience definition +ArrayOfObjectIdentifier = ArrayOf(ObjectIdentifier) + +# +# ObjectListContext +# + +class ObjectListContext: + + def __init__(self, device_id, device_addr): + self.device_id = device_id + self.device_addr = device_addr + + self.object_list = [] + self.object_names = [] + + self._object_list_queue = None + + def completed(self, had_error=None): + if had_error: + print("had error: %r" % (had_error,)) + else: + for objid, objname in zip(self.object_list, self.object_names): + print("%s: %s" % (objid, objname)) + + stop() + +# +# ReadObjectListApplication +# + +@bacpypes_debugging +class ReadObjectListApplication(BIPSimpleApplication): + + def __init__(self, *args): + if _debug: ReadObjectListApplication._debug("__init__ %r", args) + BIPSimpleApplication.__init__(self, *args) + + def read_object_list(self, device_id, device_addr): + if _debug: ReadObjectListApplication._debug("read_object_list %r %r", device_id, device_addr) + + # create a context to hold the results + context = ObjectListContext(device_id, device_addr) + + # build a request for the object name + request = ReadPropertyRequest( + destination=context.device_addr, + objectIdentifier=context.device_id, + propertyIdentifier='objectList', + ) + if _debug: ReadObjectListApplication._debug(" - request: %r", request) + + # make an IOCB, reference the context + iocb = IOCB(request) + iocb.context = context + if _debug: ReadObjectListApplication._debug(" - iocb: %r", iocb) + + # let us know when its complete + iocb.add_callback(self.object_list_results) + + # give it to the application + self.request_io(iocb) + + def object_list_results(self, iocb): + if _debug: ReadObjectListApplication._debug("object_list_results %r", iocb) + + # extract the context + context = iocb.context + + # do something for error/reject/abort + if iocb.ioError: + context.completed(iocb.ioError) + return + + # do something for success + apdu = iocb.ioResponse + + # should be an ack + if not isinstance(apdu, ReadPropertyACK): + if _debug: ReadObjectListApplication._debug(" - not an ack") + context.completed(RuntimeError("read property ack expected")) + return + + # pull out the content + object_list = apdu.propertyValue.cast_out(ArrayOfObjectIdentifier) + if _debug: ReadObjectListApplication._debug(" - object_list: %r", object_list) + + # store it in the context + context.object_list = object_list + + # make a queue of the identifiers to read, start reading them + context._object_list_queue = deque(object_list) + deferred(self.read_next_object, context) + + def read_next_object(self, context): + if _debug: ReadObjectListApplication._debug("read_next_object %r", context) + + # if there's nothing more to do, we're done + if not context._object_list_queue: + if _debug: ReadObjectListApplication._debug(" - all done") + context.completed() + return + + # pop off the next object identifier + object_id = context._object_list_queue.popleft() + if _debug: ReadObjectListApplication._debug(" - object_id: %r", object_id) + + # build a request for the object name + request = ReadPropertyRequest( + destination=context.device_addr, + objectIdentifier=object_id, + propertyIdentifier='objectName', + ) + if _debug: ReadObjectListApplication._debug(" - request: %r", request) + + # make an IOCB, reference the context + iocb = IOCB(request) + iocb.context = context + if _debug: ReadObjectListApplication._debug(" - iocb: %r", iocb) + + # let us know when its complete + iocb.add_callback(self.object_name_results) + + # give it to the application + self.request_io(iocb) + + def object_name_results(self, iocb): + if _debug: ReadObjectListApplication._debug("object_name_results %r", iocb) + + # extract the context + context = iocb.context + + # do something for error/reject/abort + if iocb.ioError: + context.completed(iocb.ioError) + return + + # do something for success + apdu = iocb.ioResponse + + # should be an ack + if not isinstance(apdu, ReadPropertyACK): + if _debug: ReadObjectListApplication._debug(" - not an ack") + context.completed(RuntimeError("read property ack expected")) + return + + # pull out the name + object_name = apdu.propertyValue.cast_out(CharacterString) + if _debug: ReadObjectListApplication._debug(" - object_name: %r", object_name) + + # store it in the context + context.object_names.append(object_name) + + # read the next one + deferred(self.read_next_object, context) + +# +# __main__ +# + +def main(): + global this_device + global this_application + + # parse the command line arguments + parser = ConfigArgumentParser(description=__doc__) + + # add an argument for interval + parser.add_argument('device_id', type=int, + help='device identifier', + ) + + # add an argument for interval + parser.add_argument('device_addr', type=str, + help='device address', + ) + + # parse the args + args = parser.parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # make a device object + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) + + # make a simple application + this_application = ReadObjectListApplication(this_device, args.ini.address) + + # build a device object identifier + device_id = ('device', args.device_id) + + # translate the address + device_addr = Address(args.device_addr) + + # kick off the process after the core is up and running + deferred(this_application.read_object_list, device_id, device_addr) + + _log.debug("running") + + run() + + _log.debug("fini") + +if __name__ == "__main__": + main() diff --git a/samples/ReadProperty.py b/samples/ReadProperty.py index f0f7dfe2..d8551e4c 100755 --- a/samples/ReadProperty.py +++ b/samples/ReadProperty.py @@ -151,13 +151,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadProperty25.py b/samples/ReadProperty25.py index abfa136c..9885b53a 100755 --- a/samples/ReadProperty25.py +++ b/samples/ReadProperty25.py @@ -153,13 +153,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadPropertyAny.py b/samples/ReadPropertyAny.py index 6b9be041..ff1c042f 100755 --- a/samples/ReadPropertyAny.py +++ b/samples/ReadPropertyAny.py @@ -17,9 +17,9 @@ from bacpypes.iocb import IOCB from bacpypes.pdu import Address -from bacpypes.object import get_datatype, get_object_class +from bacpypes.object import get_object_class -from bacpypes.apdu import ReadPropertyRequest, Error, AbortPDU, ReadPropertyACK +from bacpypes.apdu import ReadPropertyRequest from bacpypes.primitivedata import Tag from bacpypes.app import BIPSimpleApplication @@ -124,13 +124,12 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) + + # provide max segments accepted if any kind of segmentation supported + if args.ini.segmentationsupported != 'noSegmentation': + this_device.maxSegmentsAccepted = int(args.ini.maxsegmentsaccepted) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadPropertyMultiple.py b/samples/ReadPropertyMultiple.py index ca1df025..35456b78 100755 --- a/samples/ReadPropertyMultiple.py +++ b/samples/ReadPropertyMultiple.py @@ -2,8 +2,8 @@ """ This application presents a 'console' prompt to the user asking for read commands -which create ReadPropertyRequest PDUs, then lines up the coorresponding ReadPropertyACK -and prints the value. +which create ReadPropertyMultipleRequest PDUs, then lines up the coorresponding +ReadPropertyMultipleACK and prints the value. """ import sys @@ -151,7 +151,7 @@ def do_read(self, args): # here is the read result readResult = element.readResult - sys.stdout.write(propertyIdentifier) + sys.stdout.write(str(propertyIdentifier)) if propertyArrayIndex is not None: sys.stdout.write("[" + str(propertyArrayIndex) + "]") @@ -167,17 +167,17 @@ def do_read(self, args): datatype = get_datatype(objectIdentifier[0], propertyIdentifier) if _debug: ReadPropertyMultipleConsoleCmd._debug(" - datatype: %r", datatype) if not datatype: - raise TypeError("unknown datatype") - - # special case for array parts, others are managed by cast_out - if issubclass(datatype, Array) and (propertyArrayIndex is not None): - if propertyArrayIndex == 0: - value = propertyValue.cast_out(Unsigned) - else: - value = propertyValue.cast_out(datatype.subtype) + value = '?' else: - value = propertyValue.cast_out(datatype) - if _debug: ReadPropertyMultipleConsoleCmd._debug(" - value: %r", value) + # special case for array parts, others are managed by cast_out + if issubclass(datatype, Array) and (propertyArrayIndex is not None): + if propertyArrayIndex == 0: + value = propertyValue.cast_out(Unsigned) + else: + value = propertyValue.cast_out(datatype.subtype) + else: + value = propertyValue.cast_out(datatype) + if _debug: ReadPropertyMultipleConsoleCmd._debug(" - value: %r", value) sys.stdout.write(" = " + str(value) + '\n') sys.stdout.flush() @@ -203,13 +203,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadPropertyMultiple25.py b/samples/ReadPropertyMultiple25.py index 41a857b7..ab712e6d 100755 --- a/samples/ReadPropertyMultiple25.py +++ b/samples/ReadPropertyMultiple25.py @@ -209,13 +209,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadPropertyMultipleServer.py b/samples/ReadPropertyMultipleServer.py index c186599b..13c9d7df 100755 --- a/samples/ReadPropertyMultipleServer.py +++ b/samples/ReadPropertyMultipleServer.py @@ -20,6 +20,8 @@ from bacpypes.app import BIPSimpleApplication from bacpypes.service.device import DeviceCommunicationControlServices from bacpypes.service.object import ReadWritePropertyMultipleServices +from bacpypes.local.device import LocalDeviceObject + # some debugging _debug = 0 @@ -95,14 +97,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - _dcc_password="xyzzy", - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application this_application = ReadPropertyMultipleApplication(this_device, args.ini.address) diff --git a/samples/ReadPropertyMultipleServer25.py b/samples/ReadPropertyMultipleServer25.py index b483e2fc..29f3e8a3 100755 --- a/samples/ReadPropertyMultipleServer25.py +++ b/samples/ReadPropertyMultipleServer25.py @@ -95,13 +95,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application this_application = ReadPropertyMultipleApplication(this_device, args.ini.address) diff --git a/samples/ReadRange.py b/samples/ReadRange.py index a328dd10..53f77aa8 100755 --- a/samples/ReadRange.py +++ b/samples/ReadRange.py @@ -121,13 +121,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadWriteEventMessageTexts.py b/samples/ReadWriteEventMessageTexts.py index 67b24e5e..5effd7f9 100644 --- a/samples/ReadWriteEventMessageTexts.py +++ b/samples/ReadWriteEventMessageTexts.py @@ -228,15 +228,9 @@ def main(): context = args.address, args.objtype, args.objinst if _debug: _log.debug(" - context: %r", context) - # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadWriteFile.py b/samples/ReadWriteFile.py index 16fd36fa..a1a47109 100755 --- a/samples/ReadWriteFile.py +++ b/samples/ReadWriteFile.py @@ -18,7 +18,7 @@ from bacpypes.iocb import IOCB from bacpypes.pdu import Address -from bacpypes.apdu import Error, AbortPDU, \ +from bacpypes.apdu import \ AtomicReadFileRequest, \ AtomicReadFileRequestAccessMethodChoice, \ AtomicReadFileRequestAccessMethodChoiceRecordAccess, \ @@ -311,13 +311,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadWriteFileServer.py b/samples/ReadWriteFileServer.py index 407aa093..00042237 100755 --- a/samples/ReadWriteFileServer.py +++ b/samples/ReadWriteFileServer.py @@ -16,7 +16,7 @@ from bacpypes.app import BIPSimpleApplication from bacpypes.local.device import LocalDeviceObject -from bacpypes.local.file import FileServices, LocalRecordAccessFileObject, LocalStreamAccessFileObject +from bacpypes.local.file import LocalRecordAccessFileObject, LocalStreamAccessFileObject from bacpypes.service.file import FileServices # some debugging @@ -174,13 +174,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/ReadWriteProperty.py b/samples/ReadWriteProperty.py index 70e85dea..d77ff09a 100755 --- a/samples/ReadWriteProperty.py +++ b/samples/ReadWriteProperty.py @@ -271,13 +271,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/RecurringMultipleReadProperty.py b/samples/RecurringMultipleReadProperty.py index bb1826ce..a479089e 100755 --- a/samples/RecurringMultipleReadProperty.py +++ b/samples/RecurringMultipleReadProperty.py @@ -19,7 +19,7 @@ from bacpypes.pdu import Address from bacpypes.object import get_datatype -from bacpypes.apdu import ReadPropertyRequest, Error, AbortPDU, ReadPropertyACK +from bacpypes.apdu import ReadPropertyRequest from bacpypes.primitivedata import Unsigned from bacpypes.constructeddata import Array @@ -164,13 +164,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a dog this_application = PrairieDog(args.interval, this_device, args.ini.address) diff --git a/samples/ThreadedReadProperty.py b/samples/ThreadedReadProperty.py index 34f34607..6f79d717 100755 --- a/samples/ThreadedReadProperty.py +++ b/samples/ThreadedReadProperty.py @@ -161,13 +161,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/VendorAVObject.py b/samples/VendorAVObject.py index b2fc5ae6..f173e7d0 100755 --- a/samples/VendorAVObject.py +++ b/samples/VendorAVObject.py @@ -95,13 +95,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=vendor_id, - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/VendorReadWriteProperty.py b/samples/VendorReadWriteProperty.py index 9687f248..2d64493f 100755 --- a/samples/VendorReadWriteProperty.py +++ b/samples/VendorReadWriteProperty.py @@ -20,8 +20,7 @@ from bacpypes.pdu import Address from bacpypes.object import get_object_class, get_datatype -from bacpypes.apdu import Error, AbortPDU, SimpleAckPDU, \ - ReadPropertyRequest, ReadPropertyACK, WritePropertyRequest +from bacpypes.apdu import ReadPropertyRequest, WritePropertyRequest from bacpypes.primitivedata import Tag, Null, Atomic, Integer, Unsigned, Real from bacpypes.constructeddata import Array, Any @@ -243,13 +242,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/WhoIsIAm.py b/samples/WhoIsIAm.py index 1583446e..331cf265 100755 --- a/samples/WhoIsIAm.py +++ b/samples/WhoIsIAm.py @@ -17,7 +17,6 @@ from bacpypes.pdu import Address, GlobalBroadcast from bacpypes.apdu import WhoIsRequest, IAmRequest -from bacpypes.basetypes import ServicesSupported from bacpypes.errors import DecodingError from bacpypes.app import BIPSimpleApplication @@ -180,13 +179,7 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application diff --git a/samples/WhoIsIAmForeign.py b/samples/WhoIsIAmForeign.py index 2371341c..eeaa88e2 100755 --- a/samples/WhoIsIAmForeign.py +++ b/samples/WhoIsIAmForeign.py @@ -190,13 +190,7 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application diff --git a/samples/WritePropertyTCPServer.py b/samples/WritePropertyTCPServer.py index 9b9a005d..8a4f613b 100755 --- a/samples/WritePropertyTCPServer.py +++ b/samples/WritePropertyTCPServer.py @@ -226,13 +226,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/samples/WriteSomething.py b/samples/WriteSomething.py index 5aaa57bb..529399eb 100755 --- a/samples/WriteSomething.py +++ b/samples/WriteSomething.py @@ -118,13 +118,8 @@ def main(): if _debug: _log.debug(" - args: %r", args) # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) + this_device = LocalDeviceObject(ini=args.ini) + if _debug: _log.debug(" - this_device: %r", this_device) # make a simple application this_application = BIPSimpleApplication(this_device, args.ini.address) diff --git a/tests/__init__.py b/tests/__init__.py index 2935d939..d06f887e 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -27,4 +27,5 @@ from . import test_network from . import test_service from . import test_local +from . import test_segmentation diff --git a/tests/state_machine.py b/tests/state_machine.py index b7fa564d..9e4e2ddc 100755 --- a/tests/state_machine.py +++ b/tests/state_machine.py @@ -88,18 +88,18 @@ def match_pdu(pdu, pdu_type=None, **pdu_attrs): # check the type if pdu_type and not isinstance(pdu, pdu_type): - if _debug: match_pdu._debug(" - wrong 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(" - missing attr: %r", 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(" - attr value: %r, %r", 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") + if _debug: match_pdu._debug(" - successful match") return True diff --git a/tests/test_bvll/helpers.py b/tests/test_bvll/helpers.py index 5146319f..c09ee505 100644 --- a/tests/test_bvll/helpers.py +++ b/tests/test_bvll/helpers.py @@ -6,10 +6,19 @@ from bacpypes.debugging import bacpypes_debugging, ModuleLogger -from bacpypes.comm import Client, Server, bind +from bacpypes.comm import Client, Server, ApplicationServiceElement, bind from bacpypes.pdu import Address, LocalBroadcast, PDU, unpack_ip_addr from bacpypes.vlan import IPNode +from bacpypes.app import DeviceInfoCache, Application +from bacpypes.appservice import StateMachineAccessPoint, ApplicationServiceAccessPoint +from bacpypes.netservice import NetworkServiceAccessPoint, NetworkServiceElement + +from bacpypes.object import register_object_type +from bacpypes.local.device import LocalDeviceObject +from bacpypes.service.device import WhoIsIAmServices +from bacpypes.service.object import ReadWritePropertyServices + from ..state_machine import ClientStateMachine from bacpypes.bvllservice import BIPSimple, BIPForeign, BIPBBMD, AnnexJCodec @@ -26,6 +35,12 @@ @bacpypes_debugging class FauxMultiplexer(Client, Server): + """This class is a placeholder for UDPMultiplexer without the code that + determines if the upstream packets are Annex-H or Annex-J packets, it + assumes they are all Annex-J. It creates and binds itself to an IPNode + which is added to an IPNetwork. + """ + def __init__(self, addr, network=None, cid=None, sid=None): if _debug: FauxMultiplexer._debug("__init__") @@ -81,37 +96,54 @@ def confirmation(self, pdu): self.response(PDU(pdu, source=src, destination=dest)) # -# SnifferNode +# SnifferStateMachine # @bacpypes_debugging -class SnifferNode(ClientStateMachine): +class SnifferStateMachine(ClientStateMachine): + + """This class acts as a sniffer for BVLL messages. The client state + machine sits above an Annex-J codec so the send and receive PDUs are + BVLL PDUs. + """ def __init__(self, address, vlan): - if _debug: SnifferNode._debug("__init__ %r %r", address, vlan) + if _debug: SnifferStateMachine._debug("__init__ %r %r", address, vlan) ClientStateMachine.__init__(self) # save the name and address self.name = address self.address = Address(address) - # create a promiscuous node, added to the network - self.node = IPNode(self.address, vlan, promiscuous=True) - if _debug: SnifferNode._debug(" - node: %r", self.node) + # BACnet/IP interpreter + self.annexj = AnnexJCodec() - # bind this to the node - bind(self, self.node) + # fake multiplexer has a VLAN node in it + self.mux = FauxMultiplexer(self.address, vlan) + + # might receive all packets and allow spoofing + self.mux.node.promiscuous = True + self.mux.node.spoofing = True + + # bind the stack together + bind(self, self.annexj, self.mux) # -# CodecNode +# BIPStateMachine # @bacpypes_debugging -class CodecNode(ClientStateMachine): +class BIPStateMachine(ClientStateMachine): + + """This class is an application layer for BVLL messages that has no BVLL + processing like the 'simple', 'foreign', or 'bbmd' versions. The client + state machine sits above and Annex-J codec so the send and receive PDUs are + BVLL PDUs. + """ def __init__(self, address, vlan): - if _debug: CodecNode._debug("__init__ %r %r", address, vlan) + if _debug: BIPStateMachine._debug("__init__ %r %r", address, vlan) ClientStateMachine.__init__(self) # save the name and address @@ -129,14 +161,18 @@ def __init__(self, address, vlan): # -# SimpleNode +# BIPSimpleStateMachine # @bacpypes_debugging -class SimpleNode(ClientStateMachine): +class BIPSimpleStateMachine(ClientStateMachine): + + """This class sits on a BIPSimple instance, the send() and receive() + parameters are NPDUs. + """ def __init__(self, address, vlan): - if _debug: SimpleNode._debug("__init__ %r %r", address, vlan) + if _debug: BIPSimpleStateMachine._debug("__init__ %r %r", address, vlan) ClientStateMachine.__init__(self) # save the name and address @@ -155,14 +191,18 @@ def __init__(self, address, vlan): # -# ForeignNode +# BIPForeignStateMachine # @bacpypes_debugging -class ForeignNode(ClientStateMachine): +class BIPForeignStateMachine(ClientStateMachine): + + """This class sits on a BIPForeign instance, the send() and receive() + parameters are NPDUs. + """ def __init__(self, address, vlan): - if _debug: ForeignNode._debug("__init__ %r %r", address, vlan) + if _debug: BIPForeignStateMachine._debug("__init__ %r %r", address, vlan) ClientStateMachine.__init__(self) # save the name and address @@ -180,14 +220,18 @@ def __init__(self, address, vlan): bind(self, self.bip, self.annexj, self.mux) # -# BBMDNode +# BIPBBMDStateMachine # @bacpypes_debugging -class BBMDNode(ClientStateMachine): +class BIPBBMDStateMachine(ClientStateMachine): + + """This class sits on a BIPBBMD instance, the send() and receive() + parameters are NPDUs. + """ def __init__(self, address, vlan): - if _debug: BBMDNode._debug("__init__ %r %r", address, vlan) + if _debug: BIPBBMDStateMachine._debug("__init__ %r %r", address, vlan) ClientStateMachine.__init__(self) # save the name and address @@ -200,7 +244,7 @@ def __init__(self, address, vlan): # build an address, full mask bdt_address = "%s/32:%d" % self.address.addrTuple - if _debug: BBMDNode._debug(" - bdt_address: %r", bdt_address) + if _debug: BIPBBMDStateMachine._debug(" - bdt_address: %r", bdt_address) # add itself as the first entry in the BDT self.bip.add_peer(Address(bdt_address)) @@ -211,3 +255,209 @@ def __init__(self, address, vlan): # bind the stack together bind(self, self.bip, self.annexj, self.mux) +# +# BIPSimpleNode +# + +@bacpypes_debugging +class BIPSimpleNode: + + """This class is a BIPSimple instance that is not bound to a state machine.""" + + def __init__(self, address, vlan): + if _debug: BIPSimpleNode._debug("__init__ %r %r", address, vlan) + + # save the name and address + self.name = address + self.address = Address(address) + + # BACnet/IP interpreter + self.bip = BIPSimple() + self.annexj = AnnexJCodec() + + # fake multiplexer has a VLAN node in it + self.mux = FauxMultiplexer(self.address, vlan) + + # bind the stack together + bind(self.bip, self.annexj, self.mux) + +# +# BIPBBMDNode +# + +@bacpypes_debugging +class BIPBBMDNode: + + """This class is a BIPBBMD instance that is not bound to a state machine.""" + + def __init__(self, address, vlan): + if _debug: BIPBBMDNode._debug("__init__ %r %r", address, vlan) + + # save the name and address + self.name = address + self.address = Address(address) + if _debug: BIPBBMDNode._debug(" - address: %r", self.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: BIPBBMDNode._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.bip, self.annexj, self.mux) + + +# +# TestDeviceObject +# + +@register_object_type(vendor_id=999) +class TestDeviceObject(LocalDeviceObject): + + pass + +# +# BIPSimpleApplicationLayerStateMachine +# + +@bacpypes_debugging +class BIPSimpleApplicationLayerStateMachine(ApplicationServiceElement, ClientStateMachine): + + def __init__(self, address, vlan): + if _debug: BIPSimpleApplicationLayerStateMachine._debug("__init__ %r %r", address, vlan) + + # build a name, save the address + self.name = "app @ %s" % (address,) + self.address = Address(address) + + # build a local device object + local_device = TestDeviceObject( + objectName=self.name, + objectIdentifier=('device', 998), + vendorIdentifier=999, + ) + + # build an address and save it + self.address = Address(address) + if _debug: BIPSimpleApplicationLayerStateMachine._debug(" - address: %r", self.address) + + # continue with initialization + ApplicationServiceElement.__init__(self) + ClientStateMachine.__init__(self, name=local_device.objectName) + + # include a application decoder + self.asap = ApplicationServiceAccessPoint() + + # pass the device object to the state machine access point so it + # can know if it should support segmentation + self.smap = StateMachineAccessPoint(local_device) + + # the segmentation state machines need access to some device + # information cache, usually shared with the application + self.smap.deviceInfoCache = DeviceInfoCache() + + # a network service access point will be needed + self.nsap = NetworkServiceAccessPoint() + + # give the NSAP a generic network layer service element + self.nse = NetworkServiceElement() + bind(self.nse, self.nsap) + + # bind the top layers + bind(self, self.asap, self.smap, self.nsap) + + # BACnet/IP interpreter + self.bip = BIPSimple() + self.annexj = AnnexJCodec() + + # fake multiplexer has a VLAN node in it + self.mux = FauxMultiplexer(self.address, vlan) + + # bind the stack together + bind(self.bip, self.annexj, self.mux) + + # bind the stack to the local network + self.nsap.bind(self.bip) + + def indication(self, apdu): + if _debug: BIPSimpleApplicationLayerStateMachine._debug("indication %r", apdu) + self.receive(apdu) + + def confirmation(self, apdu): + if _debug: BIPSimpleApplicationLayerStateMachine._debug("confirmation %r %r", apdu) + self.receive(apdu) + +# +# BIPBBMDApplication +# + +class BIPBBMDApplication(Application, WhoIsIAmServices, ReadWritePropertyServices): + + def __init__(self, address, vlan): + if _debug: BIPBBMDApplication._debug("__init__ %r %r", address, vlan) + + # build a name, save the address + self.name = "app @ %s" % (address,) + self.address = Address(address) + if _debug: BIPBBMDApplication._debug(" - address: %r", self.address) + + # build a local device object + local_device = TestDeviceObject( + objectName=self.name, + objectIdentifier=('device', 999), + vendorIdentifier=999, + ) + + # continue with initialization + Application.__init__(self, local_device, self.address) + + # include a application decoder + self.asap = ApplicationServiceAccessPoint() + + # pass the device object to the state machine access point so it + # can know if it should support segmentation + self.smap = StateMachineAccessPoint(local_device) + + # the segmentation state machines need access to the same device + # information cache as the application + self.smap.deviceInfoCache = self.deviceInfoCache + + # a network service access point will be needed + self.nsap = NetworkServiceAccessPoint() + + # give the NSAP a generic network layer service element + self.nse = NetworkServiceElement() + bind(self.nse, self.nsap) + + # bind the top layers + bind(self, self.asap, self.smap, self.nsap) + + # 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: BIPBBMDNode._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.bip, self.annexj, self.mux) + + # bind the stack to the local network + self.nsap.bind(self.bip) + diff --git a/tests/test_bvll/test_bbmd.py b/tests/test_bvll/test_bbmd.py index fdffa2a0..602df2a7 100644 --- a/tests/test_bvll/test_bbmd.py +++ b/tests/test_bvll/test_bbmd.py @@ -1 +1,313 @@ -# placeholder +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Test BBMD +--------- +""" + +import unittest + +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 ( + Result, + WriteBroadcastDistributionTable, + ReadBroadcastDistributionTable, ReadBroadcastDistributionTableAck, + ForwardedNPDU, + RegisterForeignDevice, + ReadForeignDeviceTable, ReadForeignDeviceTableAck, + DeleteForeignDeviceTableEntry, + DistributeBroadcastToNetwork, + OriginalUnicastNPDU, + OriginalBroadcastNPDU, + ) + +from bacpypes.apdu import ( + WhoIsRequest, IAmRequest, + ReadPropertyRequest, ReadPropertyACK, + AbortPDU, + ) + +from ..state_machine import StateMachineGroup, TrafficLog +from ..time_machine import reset_time_machine, run_time_machine + +from .helpers import ( + SnifferStateMachine, BIPStateMachine, BIPSimpleStateMachine, + BIPForeignStateMachine, BIPBBMDStateMachine, + BIPSimpleNode, BIPBBMDNode, + BIPSimpleApplicationLayerStateMachine, + BIPBBMDApplication, + ) + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +# +# TNetwork +# + +@bacpypes_debugging +class TNetwork(StateMachineGroup): + + def __init__(self, count): + if _debug: TNetwork._debug("__init__ %r", count) + StateMachineGroup.__init__(self) + + # reset the time machine + reset_time_machine() + if _debug: TNetwork._debug(" - time machine reset") + + # create a traffic log + self.traffic_log = TrafficLog() + + # make a router + self.router = IPRouter() + + # make the networks + self.vlan = [] + for net in range(1, count + 1): + # make a network and set the traffic log + ip_network = IPNetwork("192.168.{}.0/24".format(net)) + ip_network.traffic_log = self.traffic_log + + # make a router + router_address = Address("192.168.{}.1/24".format(net)) + self.router.add_network(router_address, ip_network) + + self.vlan.append(ip_network) + + def run(self, time_limit=60.0): + if _debug: TNetwork._debug("run %r", time_limit) + + # run the group + super(TNetwork, self).run() + + # run it for some time + run_time_machine(time_limit) + if _debug: TNetwork._debug(" - time machine finished") + + # check for success + all_success, some_failed = super(TNetwork, self).check_for_success() + if _debug: + TNetwork._debug(" - all_success, some_failed: %r, %r", all_success, some_failed) + for state_machine in self.state_machines: + if state_machine.running: + TNetwork._debug(" %r (running)", state_machine) + elif not state_machine.current_state: + TNetwork._debug(" %r (not started)", state_machine) + else: + TNetwork._debug(" %r", state_machine) + for direction, pdu in state_machine.transaction_log: + TNetwork._debug(" %s %r", direction, pdu) + + # traffic log has what was processed on each vlan + self.traffic_log.dump(TNetwork._debug) + + assert all_success + + +@bacpypes_debugging +class TestNonBBMD(unittest.TestCase): + + def setup_method(self, method): + """This function is called before each test method is called and is + given a reference to the test method.""" + if _debug: TestNonBBMD._debug("setup_method %r", method) + + # create a network + self.tnet = TNetwork(1) + + # test device + self.td = BIPStateMachine("192.168.1.2/24", self.tnet.vlan[0]) + self.tnet.append(self.td) + + # implementation under test + self.iut = BIPSimpleNode("192.168.1.3/24", self.tnet.vlan[0]) + + def test_write_bdt_fail(self): + """Test writing a BDT.""" + if _debug: TestNonBBMD._debug("test_write_bdt_fail") + + # read the broadcast distribution table, get a nack + self.td.start_state.doc("1-1-0") \ + .send(WriteBroadcastDistributionTable(destination=self.iut.address)).doc("1-1-1") \ + .receive(Result, bvlciResultCode=0x0010).doc("1-1-2") \ + .success() + + # run the group + self.tnet.run() + + def test_read_bdt_fail(self): + """Test reading a BDT.""" + if _debug: TestNonBBMD._debug("test_read_bdt_fail") + + # read the broadcast distribution table, get a nack + self.td.start_state.doc("1-2-0") \ + .send(ReadBroadcastDistributionTable(destination=self.iut.address)).doc("1-2-1") \ + .receive(Result, bvlciResultCode=0x0020).doc("1-2-2") \ + .success() + + # run the group + self.tnet.run() + + def test_register_fail(self): + """Test registering as a foreign device to a non-BBMD.""" + if _debug: TestNonBBMD._debug("test_read_fdt_success") + + # read the broadcast distribution table, get a nack + self.td.start_state.doc("1-3-0") \ + .send(RegisterForeignDevice(10, destination=self.iut.address)).doc("1-3-1") \ + .receive(Result, bvlciResultCode=0x0030).doc("1-3-2") \ + .success() + + # run the group + self.tnet.run() + + def test_read_fdt_fail(self): + """Test reading an FDT from a non-BBMD.""" + if _debug: TestNonBBMD._debug("test_read_fdt_success") + + # read the broadcast distribution table, get a nack + self.td.start_state.doc("1-4-0") \ + .send(ReadForeignDeviceTable(destination=self.iut.address)).doc("1-4-1") \ + .receive(Result, bvlciResultCode=0x0040).doc("1-4-2") \ + .success() + + # run the group + self.tnet.run() + + def test_delete_fail(self): + """Test deleting an FDT entry from a non-BBMD.""" + if _debug: TestNonBBMD._debug("test_delete_fail") + + # read the broadcast distribution table, get a nack + self.td.start_state.doc("1-5-0") \ + .send(DeleteForeignDeviceTableEntry(Address("1.2.3.4"), destination=self.iut.address)).doc("1-5-1") \ + .receive(Result, bvlciResultCode=0x0050).doc("1-5-2") \ + .success() + + # run the group + self.tnet.run() + + def test_distribute_fail(self): + """Test asking a non-BBMD to distribute a broadcast.""" + if _debug: TestNonBBMD._debug("test_delete_fail") + + # read the broadcast distribution table, get a nack + self.td.start_state.doc("1-6-0") \ + .send(DistributeBroadcastToNetwork(xtob('deadbeef'), destination=self.iut.address)).doc("1-6-1") \ + .receive(Result, bvlciResultCode=0x0060).doc("1-6-2") \ + .success() + + # run the group + self.tnet.run() + + +@bacpypes_debugging +class TestBBMD(unittest.TestCase): + + def test_14_2_1_1(self): + """14.2.1.1 Execute Forwarded-NPDU (One-hop Distribution).""" + if _debug: TestBBMD._debug("test_14_2_1_1") + + # create a network + tnet = TNetwork(2) + + # implementation under test + iut = BIPBBMDApplication("192.168.1.2/24", tnet.vlan[0]) + if _debug: TestBBMD._debug(" - iut.bip: %r", iut.bip) + + # BBMD on net 2 + bbmd1 = BIPBBMDNode("192.168.2.2/24", tnet.vlan[1]) + + # add the IUT as a one-hop peer + bbmd1.bip.add_peer(Address("192.168.1.2/24")) + if _debug: TestBBMD._debug(" - bbmd1.bip: %r", bbmd1.bip) + + # test device + td = BIPSimpleApplicationLayerStateMachine("192.168.2.3/24", tnet.vlan[1]) + tnet.append(td) + + # listener looks for extra traffic + listener = BIPStateMachine("192.168.1.3/24", tnet.vlan[0]) + listener.mux.node.promiscuous = True + tnet.append(listener) + + # broadcast a forwarded NPDU + td.start_state.doc("2-1-0") \ + .send(WhoIsRequest(destination=LocalBroadcast())).doc("2-1-1") \ + .receive(IAmRequest).doc("2-1-2") \ + .success() + + # listen for the directed broadcast, then the original unicast, + # then fail if there's anything else + listener.start_state.doc("2-2-0") \ + .receive(ForwardedNPDU).doc("2-2-1") \ + .receive(OriginalUnicastNPDU).doc("2-2-2") \ + .timeout(3).doc("2-2-3") \ + .success() + + # run the group + tnet.run() + + def test_14_2_1_2(self): + """14.2.1.1 Execute Forwarded-NPDU (Two-hop Distribution).""" + if _debug: TestBBMD._debug("test_14_2_1_2") + + # create a network + tnet = TNetwork(2) + + # implementation under test + iut = BIPBBMDApplication("192.168.1.2/24", tnet.vlan[0]) + if _debug: TestBBMD._debug(" - iut.bip: %r", iut.bip) + + # BBMD on net 2 + bbmd1 = BIPBBMDNode("192.168.2.2/24", tnet.vlan[1]) + + # add the IUT as a two-hop peer + bbmd1.bip.add_peer(Address("192.168.1.2/32")) + if _debug: TestBBMD._debug(" - bbmd1.bip: %r", bbmd1.bip) + + # test device + td = BIPSimpleApplicationLayerStateMachine("192.168.2.3/24", tnet.vlan[1]) + tnet.append(td) + + # listener looks for extra traffic + listener = BIPStateMachine("192.168.1.3/24", tnet.vlan[0]) + listener.mux.node.promiscuous = True + tnet.append(listener) + + # broadcast a forwarded NPDU + td.start_state.doc("2-3-0") \ + .send(WhoIsRequest(destination=LocalBroadcast())).doc("2-3-1") \ + .receive(IAmRequest).doc("2-3-2") \ + .success() + + # listen for the forwarded NPDU. The packet will be sent upstream which + # will generate the original unicast going back, then it will be + # re-broadcast on the local LAN. Fail if there's anything after that. + s241 = listener.start_state.doc("2-4-0") \ + .receive(ForwardedNPDU).doc("2-4-1") + + # look for the original unicast going back, followed by the rebroadcast + # of the forwarded NPDU on the local LAN + both = s241 \ + .receive(OriginalUnicastNPDU).doc("2-4-1-a") \ + .receive(ForwardedNPDU).doc("2-4-1-b") + + # fail if anything is received after both packets + both.timeout(3).doc("2-4-4") \ + .success() + + # allow the two packets in either order + s241.receive(ForwardedNPDU).doc("2-4-2-a") \ + .receive(OriginalUnicastNPDU, next_state=both).doc("2-4-2-b") + + # run the group + tnet.run() + diff --git a/tests/test_bvll/test_foreign.py b/tests/test_bvll/test_foreign.py index 521b527f..0fa157b7 100644 --- a/tests/test_bvll/test_foreign.py +++ b/tests/test_bvll/test_foreign.py @@ -12,12 +12,20 @@ from bacpypes.pdu import Address, PDU, LocalBroadcast from bacpypes.vlan import IPNetwork, IPRouter -from bacpypes.bvll import ReadForeignDeviceTable, ReadForeignDeviceTableAck - -from ..state_machine import StateMachineGroup +from bacpypes.bvll import ( + Result, RegisterForeignDevice, + ReadForeignDeviceTable, ReadForeignDeviceTableAck, + DistributeBroadcastToNetwork, ForwardedNPDU, + OriginalUnicastNPDU, OriginalBroadcastNPDU, + ) + +from ..state_machine import StateMachineGroup, TrafficLog from ..time_machine import reset_time_machine, run_time_machine -from .helpers import SnifferNode, CodecNode, SimpleNode, ForeignNode, BBMDNode +from .helpers import ( + SnifferStateMachine, BIPStateMachine, + BIPSimpleStateMachine, BIPForeignStateMachine, BIPBBMDStateMachine, + ) # some debugging _debug = 0 @@ -39,23 +47,28 @@ def __init__(self): reset_time_machine() if _debug: TNetwork._debug(" - time machine reset") + # create a traffic log + self.traffic_log = TrafficLog() + # make a router self.router = IPRouter() # make a home LAN - self.home_vlan = IPNetwork() - self.router.add_network(Address("192.168.5.1/24"), self.home_vlan) + self.vlan_5 = IPNetwork("192.168.5.0/24") + self.vlan_5.traffic_log = self.traffic_log + self.router.add_network(Address("192.168.5.1/24"), self.vlan_5) # make a remote LAN - self.remote_vlan = IPNetwork() - self.router.add_network(Address("192.168.6.1/24"), self.remote_vlan) + self.vlan_6 = IPNetwork("192.168.6.0/24") + self.vlan_6.traffic_log = self.traffic_log + self.router.add_network(Address("192.168.6.1/24"), self.vlan_6) # the foreign device - self.fd = ForeignNode("192.168.6.2/24", self.remote_vlan) + self.fd = BIPForeignStateMachine("192.168.6.2/24", self.vlan_6) self.append(self.fd) # bbmd - self.bbmd = BBMDNode("192.168.5.3/24", self.home_vlan) + self.bbmd = BIPBBMDStateMachine("192.168.5.3/24", self.vlan_5) self.append(self.bbmd) def run(self, time_limit=60.0): @@ -70,6 +83,22 @@ def run(self, time_limit=60.0): # check for success all_success, some_failed = super(TNetwork, self).check_for_success() + + if _debug: + TNetwork._debug(" - all_success, some_failed: %r, %r", all_success, some_failed) + for state_machine in self.state_machines: + if state_machine.running: + TNetwork._debug(" %r (running)", state_machine) + elif not state_machine.current_state: + TNetwork._debug(" %r (not started)", state_machine) + else: + TNetwork._debug(" %r", state_machine) + for direction, pdu in state_machine.transaction_log: + TNetwork._debug(" %s %s", direction, str(pdu)) + + # traffic log has what was processed on each vlan + self.traffic_log.dump(TNetwork._debug) + assert all_success @@ -97,58 +126,46 @@ def test_registration(self): # 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 = xtob('81.05.0006' # bvlci - '001e' # time-to-live - ) - registration_ack = xtob('81.00.0006.0000') # simple ack + # remote sniffer node + remote_sniffer = SnifferStateMachine("192.168.6.254/24", tnet.vlan_6) + tnet.append(remote_sniffer) - # remote sniffer sees registration + # sniffer traffic remote_sniffer.start_state.doc("1-1-0") \ - .receive(PDU, pduData=registration_request).doc("1-1-1") \ - .receive(PDU, pduData=registration_ack).doc("1-1-2") \ + .receive(RegisterForeignDevice).doc("1-1-1") \ + .receive(Result).doc("1-1-2") \ .set_event('fd-registered').doc("1-1-3") \ .success() # the bbmd is idle tnet.bbmd.start_state.success() - # read the FDT - cnode.start_state.doc("1-2-0") \ + # home snooper node + home_snooper = BIPStateMachine("192.168.5.2/24", tnet.vlan_5) + tnet.append(home_snooper) + + # snooper will read the foreign device table + home_snooper.start_state.doc("1-2-0") \ .wait_event('fd-registered').doc("1-2-1") \ .send(ReadForeignDeviceTable(destination=tnet.bbmd.address)).doc("1-2-2") \ .receive(ReadForeignDeviceTableAck).doc("1-2-3") \ .success() - # the tnode reads the registration table - read_fdt_request = xtob('81.06.0004') # bvlci - read_fdt_ack = xtob('81.07.000e' # read-ack - 'c0.a8.06.02.ba.c0 001e 0023' # address, ttl, remaining - ) + # home sniffer node + home_sniffer = SnifferStateMachine("192.168.5.254/24", tnet.vlan_5) + tnet.append(home_sniffer) - # home sniffer sees registration + # sniffer traffic home_sniffer.start_state.doc("1-3-0") \ - .receive(PDU, pduData=registration_request).doc("1-3-1") \ - .receive(PDU, pduData=registration_ack).doc("1-3-2") \ - .receive(PDU, pduData=read_fdt_request).doc("1-3-3") \ - .receive(PDU, pduData=read_fdt_ack).doc("1-3-4") \ + .receive(RegisterForeignDevice).doc("1-3-1") \ + .receive(Result).doc("1-3-2") \ + .receive(ReadForeignDeviceTable).doc("1-3-3") \ + .receive(ReadForeignDeviceTableAck).doc("1-3-4") \ .success() # run the group @@ -170,21 +187,15 @@ def test_refresh_registration(self): tnet.bbmd.start_state.success() # remote sniffer node - remote_sniffer = SnifferNode("192.168.6.254/24", tnet.remote_vlan) + remote_sniffer = SnifferStateMachine("192.168.6.254/24", tnet.vlan_6) tnet.append(remote_sniffer) - # sniffer pieces - registration_request = xtob('81.05.0006' # bvlci - '000a' # time-to-live - ) - registration_ack = xtob('81.00.0006.0000') # simple ack - - # remote sniffer sees registration + # sniffer traffic remote_sniffer.start_state.doc("2-1-0") \ - .receive(PDU, pduData=registration_request).doc("2-1-1") \ - .receive(PDU, pduData=registration_ack).doc("2-1-2") \ - .receive(PDU, pduData=registration_request).doc("2-1-3") \ - .receive(PDU, pduData=registration_ack).doc("2-1-4") \ + .receive(RegisterForeignDevice).doc("2-1-1") \ + .receive(Result).doc("2-1-2") \ + .receive(RegisterForeignDevice).doc("2-1-3") \ + .receive(Result).doc("2-1-4") \ .success() # run the group @@ -205,7 +216,7 @@ def test_unicast(self): # register, wait for ack, send some beef tnet.fd.start_state.doc("3-1-0") \ .call(tnet.fd.bip.register, tnet.bbmd.address, 60).doc("3-1-1") \ - .wait_event('fd-registered').doc("3-1-2") \ + .wait_event('3-registered').doc("3-1-2") \ .send(pdu).doc("3-1-3") \ .success() @@ -215,24 +226,15 @@ def test_unicast(self): .success() # remote sniffer node - remote_sniffer = SnifferNode("192.168.6.254/24", tnet.remote_vlan) + remote_sniffer = SnifferStateMachine("192.168.6.254/24", tnet.vlan_6) tnet.append(remote_sniffer) - # sniffer pieces - registration_request = xtob('81.05.0006' # bvlci - '003c' # time-to-live (60) - ) - registration_ack = xtob('81.00.0006.0000') # simple ack - unicast_pdu = xtob('81.0a.0008' # original unicast bvlci - 'dead.beef' # PDU being unicast - ) - - # remote sniffer sees registration + # sniffer traffic remote_sniffer.start_state.doc("3-2-0") \ - .receive(PDU, pduData=registration_request).doc("3-2-1") \ - .receive(PDU, pduData=registration_ack).doc("3-2-2") \ - .set_event('fd-registered').doc("3-2-3") \ - .receive(PDU, pduData=unicast_pdu).doc("3-2-4") \ + .receive(RegisterForeignDevice).doc("3-2-1") \ + .receive(Result).doc("3-2-2") \ + .set_event('3-registered').doc("3-2-3") \ + .receive(OriginalUnicastNPDU).doc("3-2-4") \ .success() # run the group @@ -258,38 +260,29 @@ def test_broadcast(self): .success() # the bbmd is happy when it gets the pdu - tnet.bbmd.start_state \ - .receive(PDU, pduSource=tnet.fd.address, pduData=pdu_data) \ + tnet.bbmd.start_state.doc("4-2-0") \ + .receive(PDU, pduSource=tnet.fd.address, pduData=pdu_data).doc("4-2-1") \ .success() - # home sniffer node - home_node = SimpleNode("192.168.5.254/24", tnet.home_vlan) + # home simple node + home_node = BIPSimpleStateMachine("192.168.5.254/24", tnet.vlan_5) tnet.append(home_node) # home node happy when getting the pdu, broadcast by the bbmd - home_node.start_state.doc("4-2-0") \ - .receive(PDU, pduSource=tnet.fd.address, pduData=pdu_data).doc("4-2-1") \ + home_node.start_state.doc("4-3-0") \ + .receive(PDU, pduSource=tnet.fd.address, pduData=pdu_data).doc("4-3-1") \ .success() # remote sniffer node - remote_sniffer = SnifferNode("192.168.6.254/24", tnet.remote_vlan) + remote_sniffer = SnifferStateMachine("192.168.6.254/24", tnet.vlan_6) tnet.append(remote_sniffer) - # sniffer pieces - registration_request = xtob('81.05.0006' # bvlci - '003c' # time-to-live (60) - ) - registration_ack = xtob('81.00.0006.0000') # simple ack - distribute_pdu = xtob('81.09.0008' # bvlci - 'deadbeef' # PDU to broadcast - ) - - # remote sniffer sees registration - remote_sniffer.start_state.doc("4-3-0") \ - .receive(PDU, pduData=registration_request).doc("4-3-1") \ - .receive(PDU, pduData=registration_ack).doc("4-3-2") \ + # remote traffic + remote_sniffer.start_state.doc("4-4-0") \ + .receive(RegisterForeignDevice).doc("4-4-1") \ + .receive(Result).doc("4-4-2") \ .set_event('4-registered') \ - .receive(PDU, pduData=distribute_pdu).doc("4-3-3") \ + .receive(DistributeBroadcastToNetwork).doc("4-4-3") \ .success() # run the group diff --git a/tests/test_bvll/test_simple.py b/tests/test_bvll/test_simple.py index fccf8cf0..729b9d0e 100644 --- a/tests/test_bvll/test_simple.py +++ b/tests/test_bvll/test_simple.py @@ -11,12 +11,15 @@ from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob from bacpypes.pdu import PDU, LocalBroadcast +from bacpypes.bvll import OriginalUnicastNPDU, OriginalBroadcastNPDU from bacpypes.vlan import IPNetwork from ..state_machine import match_pdu, StateMachineGroup from ..time_machine import reset_time_machine, run_time_machine -from .helpers import SnifferNode, SimpleNode +from .helpers import ( + SnifferStateMachine, BIPSimpleStateMachine, + ) # some debugging _debug = 0 @@ -42,15 +45,15 @@ def __init__(self): self.vlan = IPNetwork() # test device - self.td = SimpleNode("192.168.4.1/24", self.vlan) + self.td = BIPSimpleStateMachine("192.168.4.1/24", self.vlan) self.append(self.td) # implementation under test - self.iut = SimpleNode("192.168.4.2/24", self.vlan) + self.iut = BIPSimpleStateMachine("192.168.4.2/24", self.vlan) self.append(self.iut) # sniffer node - self.sniffer = SnifferNode("192.168.4.254/24", self.vlan) + self.sniffer = SnifferStateMachine("192.168.4.254/24", self.vlan) self.append(self.sniffer) @@ -109,12 +112,10 @@ def test_unicast(self): tnet.iut.start_state.receive(PDU, pduSource=tnet.td.address).success() # sniffer sees message on the wire - tnet.sniffer.start_state.receive(PDU, + tnet.sniffer.start_state.receive(OriginalUnicastNPDU, pduSource=tnet.td.address.addrTuple, pduDestination=tnet.iut.address.addrTuple, - pduData=xtob('81.0a.0008' # original unicast bvlci - 'deadbeef' # PDU being unicast - ), + pduData=pdu_data, ).timeout(1.0).success() # run the group @@ -137,12 +138,10 @@ def test_broadcast(self): tnet.iut.start_state.receive(PDU, pduSource=tnet.td.address).success() # sniffer sees message on the wire - tnet.sniffer.start_state.receive(PDU, - pduSource=tnet.td.address.addrTuple, - pduDestination=('192.168.4.255', 47808), - pduData=xtob('81.0b.0008' # original broadcast bvlci - 'deadbeef' # PDU being unicast - ), + tnet.sniffer.start_state.receive(OriginalBroadcastNPDU, + pduSource=tnet.td.address.addrTuple, +# pduDestination=('192.168.4.255', 47808), + pduData=pdu_data, ).timeout(1.0).success() # run the group diff --git a/tests/test_network/helpers.py b/tests/test_network/helpers.py index 1c039e41..a409ffb6 100644 --- a/tests/test_network/helpers.py +++ b/tests/test_network/helpers.py @@ -71,14 +71,14 @@ def confirmation(self, pdu): # -# SnifferNode +# SnifferStateMachine # @bacpypes_debugging -class SnifferNode(ClientStateMachine): +class SnifferStateMachine(ClientStateMachine): def __init__(self, address, vlan): - if _debug: SnifferNode._debug("__init__ %r %r", address, vlan) + if _debug: SnifferStateMachine._debug("__init__ %r %r", address, vlan) ClientStateMachine.__init__(self) # save the name and address @@ -87,20 +87,20 @@ def __init__(self, address, vlan): # create a promiscuous node, added to the network self.node = Node(self.address, vlan, promiscuous=True) - if _debug: SnifferNode._debug(" - node: %r", self.node) + if _debug: SnifferStateMachine._debug(" - node: %r", self.node) # bind this to the node bind(self, self.node) # -# NetworkLayerNode +# NetworkLayerStateMachine # @bacpypes_debugging -class NetworkLayerNode(ClientStateMachine): +class NetworkLayerStateMachine(ClientStateMachine): def __init__(self, address, vlan): - if _debug: NetworkLayerNode._debug("__init__ %r %r", address, vlan) + if _debug: NetworkLayerStateMachine._debug("__init__ %r %r", address, vlan) ClientStateMachine.__init__(self) # save the name and address @@ -109,11 +109,11 @@ def __init__(self, address, vlan): # create a network layer encoder/decoder self.codec = NPDUCodec() - if _debug: SnifferNode._debug(" - codec: %r", self.codec) + if _debug: SnifferStateMachine._debug(" - codec: %r", self.codec) # create a node, added to the network self.node = Node(self.address, vlan) - if _debug: SnifferNode._debug(" - node: %r", self.node) + if _debug: SnifferStateMachine._debug(" - node: %r", self.node) # bind this to the node bind(self, self.codec, self.node) @@ -158,14 +158,14 @@ class TestDeviceObject(LocalDeviceObject): pass # -# ApplicationLayerNode +# ApplicationLayerStateMachine # @bacpypes_debugging -class ApplicationLayerNode(ApplicationServiceElement, ClientStateMachine): +class ApplicationLayerStateMachine(ApplicationServiceElement, ClientStateMachine): def __init__(self, address, vlan): - if _debug: ApplicationLayerNode._debug("__init__ %r %r", address, vlan) + if _debug: ApplicationLayerStateMachine._debug("__init__ %r %r", address, vlan) # build a name, save the address self.name = "app @ %s" % (address,) @@ -180,7 +180,7 @@ def __init__(self, address, vlan): # build an address and save it self.address = Address(address) - if _debug: ApplicationLayerNode._debug(" - address: %r", self.address) + if _debug: ApplicationLayerStateMachine._debug(" - address: %r", self.address) # continue with initialization ApplicationServiceElement.__init__(self) @@ -209,17 +209,17 @@ def __init__(self, address, vlan): # create a node, added to the network self.node = Node(self.address, vlan) - if _debug: ApplicationLayerNode._debug(" - node: %r", self.node) + if _debug: ApplicationLayerStateMachine._debug(" - node: %r", self.node) # bind the stack to the local network self.nsap.bind(self.node) def indication(self, apdu): - if _debug: ApplicationLayerNode._debug("indication %r", apdu) + if _debug: ApplicationLayerStateMachine._debug("indication %r", apdu) self.receive(apdu) def confirmation(self, apdu): - if _debug: ApplicationLayerNode._debug("confirmation %r %r", apdu) + if _debug: ApplicationLayerStateMachine._debug("confirmation %r %r", apdu) self.receive(apdu) # diff --git a/tests/test_network/test_net_1.py b/tests/test_network/test_net_1.py index 3ed474d3..f77d1d04 100644 --- a/tests/test_network/test_net_1.py +++ b/tests/test_network/test_net_1.py @@ -29,7 +29,7 @@ from ..state_machine import match_pdu, StateMachineGroup from ..time_machine import reset_time_machine, run_time_machine -from .helpers import SnifferNode, NetworkLayerNode, RouterNode +from .helpers import SnifferStateMachine, NetworkLayerStateMachine, RouterNode # some debugging _debug = 0 @@ -58,11 +58,11 @@ def __init__(self): self.vlan1 = Network(name="vlan1", broadcast_address=LocalBroadcast()) # test device - self.td = NetworkLayerNode("1", self.vlan1) + self.td = NetworkLayerStateMachine("1", self.vlan1) self.append(self.td) # sniffer node - self.sniffer1 = SnifferNode("2", self.vlan1) + self.sniffer1 = SnifferStateMachine("2", self.vlan1) self.append(self.sniffer1) # add the network @@ -72,7 +72,7 @@ def __init__(self): self.vlan2 = Network(name="vlan2", broadcast_address=LocalBroadcast()) # sniffer node - self.sniffer2 = SnifferNode("4", self.vlan2) + self.sniffer2 = SnifferStateMachine("4", self.vlan2) self.append(self.sniffer2) # add the network @@ -82,7 +82,7 @@ def __init__(self): self.vlan3 = Network(name="vlan3", broadcast_address=LocalBroadcast()) # sniffer node - self.sniffer3 = SnifferNode("6", self.vlan3) + self.sniffer3 = SnifferStateMachine("6", self.vlan3) self.append(self.sniffer3) # add the network diff --git a/tests/test_network/test_net_2.py b/tests/test_network/test_net_2.py index 10f554fc..73d65396 100644 --- a/tests/test_network/test_net_2.py +++ b/tests/test_network/test_net_2.py @@ -30,7 +30,7 @@ from ..state_machine import match_pdu, StateMachineGroup, TrafficLog from ..time_machine import reset_time_machine, run_time_machine -from .helpers import SnifferNode, NetworkLayerNode, RouterNode +from .helpers import SnifferStateMachine, NetworkLayerStateMachine, RouterNode # some debugging _debug = 0 @@ -64,11 +64,11 @@ def __init__(self): self.vlan1.traffic_log = self.traffic_log # test device - self.td = NetworkLayerNode("1", self.vlan1) + self.td = NetworkLayerStateMachine("1", self.vlan1) self.append(self.td) # sniffer node - self.sniffer1 = SnifferNode("2", self.vlan1) + self.sniffer1 = SnifferStateMachine("2", self.vlan1) self.append(self.sniffer1) # connect vlan1 to iut1 @@ -79,7 +79,7 @@ def __init__(self): self.vlan2.traffic_log = self.traffic_log # sniffer node - self.sniffer2 = SnifferNode("4", self.vlan2) + self.sniffer2 = SnifferStateMachine("4", self.vlan2) self.append(self.sniffer2) # connect vlan2 to both routers @@ -91,7 +91,7 @@ def __init__(self): self.vlan3.traffic_log = self.traffic_log # sniffer node - self.sniffer3 = SnifferNode("7", self.vlan3) + self.sniffer3 = SnifferStateMachine("7", self.vlan3) self.append(self.sniffer3) # connect vlan3 to the second router diff --git a/tests/test_network/test_net_3.py b/tests/test_network/test_net_3.py index 1b8bfb78..21b4e715 100644 --- a/tests/test_network/test_net_3.py +++ b/tests/test_network/test_net_3.py @@ -35,10 +35,10 @@ ) from ..state_machine import match_pdu, StateMachineGroup -from ..time_machine import reset_time_machine, run_time_machine +from ..time_machine import reset_time_machine, run_time_machine, current_time from .helpers import ( - SnifferNode, NetworkLayerNode, RouterNode, ApplicationLayerNode, + SnifferStateMachine, NetworkLayerStateMachine, RouterNode, ApplicationLayerStateMachine, ApplicationNode, ) @@ -69,11 +69,11 @@ def __init__(self): self.vlan1 = Network(name="vlan1", broadcast_address=LocalBroadcast()) # test device - self.td = ApplicationLayerNode("1", self.vlan1) + self.td = ApplicationLayerStateMachine("1", self.vlan1) self.append(self.td) # sniffer node - self.sniffer1 = SnifferNode("2", self.vlan1) + self.sniffer1 = SnifferStateMachine("2", self.vlan1) self.append(self.sniffer1) # add the network @@ -86,7 +86,7 @@ def __init__(self): self.app2 = ApplicationNode("4", self.vlan2) # sniffer node - self.sniffer2 = SnifferNode("5", self.vlan2) + self.sniffer2 = SnifferStateMachine("5", self.vlan2) self.append(self.sniffer2) # add the network @@ -94,12 +94,16 @@ def __init__(self): def run(self, time_limit=60.0): if _debug: TNetwork._debug("run %r", time_limit) + if _debug: TNetwork._debug(" - current_time: %r", current_time()) # run the group super(TNetwork, self).run() + if _debug: TNetwork._debug(" - current_time: %r", current_time()) # run it for some time run_time_machine(time_limit) + if _debug: TNetwork._debug(" - current_time: %r", current_time()) + if _debug: TNetwork._debug(" - time machine finished") for state_machine in self.state_machines: @@ -335,27 +339,27 @@ def test_remote_read_2(self): ) ).doc("5-2-2") \ .receive(PDU, - pduData=xtob('01.24.00.02.01.04.ff' + pduData=xtob('01.24.00.02.01.04.ff' # request '02.44.01.0c.0c.02.00.00.04.19.78' ) ).doc("5-2-3") \ .receive(PDU, - pduData=xtob('01.08.00.02.01.04' + pduData=xtob('01.08.00.02.01.04' # ack '30.01.0c.0c.02.00.00.04.19.78.3e.22.03.e7.3f' ) ).doc("5-2-4") \ .timeout(3).doc("5-2-5") \ .success() - # network 2 sees local broadcast request and unicast response + # network 2 sees routed request and unicast response tnet.sniffer2.start_state.doc('5-3-0') \ .receive(PDU, - pduData=xtob('01.0c.00.01.01.01' + pduData=xtob('01.0c.00.01.01.01' # request '02.44.01.0c.0c.02.00.00.04.19.78' ) ).doc("5-3-1") \ .receive(PDU, - pduData=xtob('01.20.00.01.01.01.ff' + pduData=xtob('01.20.00.01.01.01.ff' # ack '30.01.0c.0c.02.00.00.04.19.78.3e.22.03.e7.3f' ) ).doc("5-3-2") \ diff --git a/tests/test_segmentation/__init__.py b/tests/test_segmentation/__init__.py new file mode 100644 index 00000000..c3e903a9 --- /dev/null +++ b/tests/test_segmentation/__init__.py @@ -0,0 +1,7 @@ +#!/usr/bin/python + +""" +Test Segmentation +""" + +from . import test_1 diff --git a/tests/test_segmentation/test_1.py b/tests/test_segmentation/test_1.py new file mode 100644 index 00000000..ed42de67 --- /dev/null +++ b/tests/test_segmentation/test_1.py @@ -0,0 +1,345 @@ +#!/urs/bin/python3 + +""" +Test Segmentation +""" + +import random +import string +import unittest + +from bacpypes.debugging import ModuleLogger, bacpypes_debugging, btox, xtob +from bacpypes.consolelogging import ArgumentParser + +from bacpypes.primitivedata import CharacterString +from bacpypes.constructeddata import Any + +from bacpypes.comm import Client, bind +from bacpypes.pdu import Address, LocalBroadcast +from bacpypes.vlan import Network, Node + +from bacpypes.npdu import NPDU, npdu_types +from bacpypes.apdu import APDU, apdu_types, \ + confirmed_request_types, unconfirmed_request_types, complex_ack_types, error_types, \ + ConfirmedRequestPDU, UnconfirmedRequestPDU, \ + SimpleAckPDU, ComplexAckPDU, SegmentAckPDU, ErrorPDU, RejectPDU, AbortPDU + +from bacpypes.apdu import APDU, ErrorPDU, RejectPDU, AbortPDU, \ + ConfirmedPrivateTransferRequest, ConfirmedPrivateTransferACK + +from bacpypes.app import Application +from bacpypes.appservice import StateMachineAccessPoint, ApplicationServiceAccessPoint +from bacpypes.netservice import NetworkServiceAccessPoint, NetworkServiceElement +from bacpypes.local.device import LocalDeviceObject + +from ..state_machine import StateMachine, StateMachineGroup, TrafficLog +from ..time_machine import reset_time_machine, run_time_machine + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +# +# ApplicationNetwork +# + +@bacpypes_debugging +class ApplicationNetwork(StateMachineGroup): + + def __init__(self, td_device_object, iut_device_object): + if _debug: ApplicationNetwork._debug("__init__ %r %r", td_device_object, iut_device_object) + StateMachineGroup.__init__(self) + + # reset the time machine + reset_time_machine() + if _debug: ApplicationNetwork._debug(" - time machine reset") + + # create a traffic log + self.traffic_log = TrafficLog() + + # make a little LAN + self.vlan = Network(broadcast_address=LocalBroadcast()) + self.vlan.traffic_log = self.traffic_log + + # sniffer + self.sniffer = SnifferNode(self.vlan) + + # test device + self.td = ApplicationStateMachine(td_device_object, self.vlan) + self.append(self.td) + + # implementation under test + self.iut = ApplicationStateMachine(iut_device_object, self.vlan) + self.append(self.iut) + + def run(self, time_limit=60.0): + if _debug: ApplicationNetwork._debug("run %r", time_limit) + + # run the group + super(ApplicationNetwork, self).run() + if _debug: ApplicationNetwork._debug(" - group running") + + # run it for some time + run_time_machine(time_limit) + if _debug: + ApplicationNetwork._debug(" - time machine finished") + for state_machine in self.state_machines: + ApplicationNetwork._debug(" - machine: %r", state_machine) + for direction, pdu in state_machine.transaction_log: + ApplicationNetwork._debug(" %s %s", direction, str(pdu)) + + # traffic log has what was processed on each vlan + self.traffic_log.dump(ApplicationNetwork._debug) + + # check for success + all_success, some_failed = super(ApplicationNetwork, self).check_for_success() + ApplicationNetwork._debug(" - all_success, some_failed: %r, %r", all_success, some_failed) + assert all_success + + +# +# SnifferNode +# + +@bacpypes_debugging +class SnifferNode(Client): ### , StateMachine): + + def __init__(self, vlan): + if _debug: SnifferNode._debug("__init__ %r", vlan) + + # save the name and give it a blank address + self.name = "sniffer" + self.address = Address() + + # continue with initialization + Client.__init__(self) + ### StateMachine.__init__(self) + + # create a promiscuous node, added to the network + self.node = Node(self.address, vlan, promiscuous=True) + if _debug: SnifferNode._debug(" - node: %r", self.node) + + # bind this to the node + bind(self, self.node) + + def send(self, pdu): + if _debug: SnifferNode._debug("send(%s) %r", self.name, pdu) + raise RuntimeError("sniffers don't send") + + def confirmation(self, pdu): + if _debug: SnifferNode._debug("confirmation(%s) %r", self.name, pdu) + + # it's an NPDU + npdu = NPDU() + npdu.decode(pdu) + + # decode as a generic APDU + apdu = APDU() + apdu.decode(npdu) + + # "lift" the source and destination address + if npdu.npduSADR: + apdu.pduSource = npdu.npduSADR + else: + apdu.pduSource = npdu.pduSource + if npdu.npduDADR: + apdu.pduDestination = npdu.npduDADR + else: + apdu.pduDestination = npdu.pduDestination + + # make a more focused interpretation + atype = apdu_types.get(apdu.apduType) + if _debug: SnifferNode._debug(" - atype: %r", atype) + + xpdu = apdu + apdu = atype() + apdu.decode(xpdu) + + print(repr(apdu)) + apdu.debug_contents() + print("") + +# +# ApplicationStateMachine +# + +@bacpypes_debugging +class ApplicationStateMachine(Application, StateMachine): + + def __init__(self, localDevice, vlan): + if _debug: ApplicationStateMachine._debug("__init__ %r %r", localDevice, vlan) + + # build an address and save it + self.address = Address(localDevice.objectIdentifier[1]) + if _debug: ApplicationStateMachine._debug(" - address: %r", self.address) + + # continue with initialization + Application.__init__(self, localDevice, self.address) + StateMachine.__init__(self, name=localDevice.objectName) + + # include a application decoder + self.asap = ApplicationServiceAccessPoint() + + # pass the device object to the state machine access point so it + # can know if it should support segmentation + self.smap = StateMachineAccessPoint(localDevice) + + # the segmentation state machines need access to the same device + # information cache as the application + self.smap.deviceInfoCache = self.deviceInfoCache + + # a network service access point will be needed + self.nsap = NetworkServiceAccessPoint() + + # give the NSAP a generic network layer service element + self.nse = NetworkServiceElement() + bind(self.nse, self.nsap) + + # bind the top layers + bind(self, self.asap, self.smap, self.nsap) + + # create a node, added to the network + self.node = Node(self.address, vlan) + + # bind the network service to the node, no network number + self.nsap.bind(self.node) + + # placeholder for the result block + self.confirmed_private_result = None + + def send(self, apdu): + if _debug: ApplicationStateMachine._debug("send(%s) %r", self.name, apdu) + + # send the apdu down the stack + self.request(apdu) + + def indication(self, apdu): + if _debug: ApplicationStateMachine._debug("indication(%s) %r", self.name, apdu) + + # let the state machine know the request was received + self.receive(apdu) + + # allow the application to process it + super(ApplicationStateMachine, self).indication(apdu) + + def confirmation(self, apdu): + if _debug: ApplicationStateMachine._debug("confirmation(%s) %r", self.name, apdu) + + # forward the confirmation to the state machine + self.receive(apdu) + + def do_ConfirmedPrivateTransferRequest(self, apdu): + if _debug: ApplicationStateMachine._debug("do_ConfirmedPrivateTransferRequest(%s) %r", self.name, apdu) + + # simple ack + xapdu = ConfirmedPrivateTransferACK(context=apdu) + xapdu.vendorID = 999 + xapdu.serviceNumber = 1 + xapdu.resultBlock = self.confirmed_private_result + + if _debug: ApplicationStateMachine._debug(" - xapdu: %r", xapdu) + + # send the response back + self.response(xapdu) + + +@bacpypes_debugging +class TestSegmentation(unittest.TestCase): + + def test_1(self): + """9.39.1 Unsupported Confirmed Services Test""" + if _debug: TestSegmentation._debug("test_1") + + # client device object + td_device_object = LocalDeviceObject( + objectName="td", + objectIdentifier=("device", 10), + maxApduLengthAccepted=206, + segmentationSupported='segmentedBoth', + maxSegmentsAccepted=4, + vendorIdentifier=999, + ) + + # server device object + iut_device_object = LocalDeviceObject( + objectName="iut", + objectIdentifier=("device", 20), + maxApduLengthAccepted=206, + segmentationSupported='segmentedBoth', + maxSegmentsAccepted=99, + vendorIdentifier=999, + ) + + # create a network + anet = ApplicationNetwork(td_device_object, iut_device_object) + + # client settings + c_ndpu_len = 50 + c_len = 0 + + # server settings + s_ndpu_len = 50 + s_len = 0 + + # tell the device info cache of the client about the server + if 0: + iut_device_info = anet.td.deviceInfoCache.get_device_info(anet.iut.address) + + # update the rest of the values + iut_device_info.maxApduLengthAccepted = iut_device_object.maxApduLengthAccepted + iut_device_info.segmentationSupported = iut_device_object.segmentationSupported + iut_device_info.vendorID = iut_device_object.vendorIdentifier + iut_device_info.maxSegmentsAccepted = iut_device_object.maxSegmentsAccepted + iut_device_info.maxNpduLength = s_ndpu_len + + # tell the device info cache of the server device about the client + if 0: + td_device_info = anet.iut.deviceInfoCache.get_device_info(anet.td.address) + + # update the rest of the values + td_device_info.maxApduLengthAccepted = td_device_object.maxApduLengthAccepted + td_device_info.segmentationSupported = td_device_object.segmentationSupported + td_device_info.vendorID = td_device_object.vendorIdentifier + td_device_info.maxSegmentsAccepted = td_device_object.maxSegmentsAccepted + td_device_info.maxNpduLength = c_ndpu_len + + # build a request string + if c_len: + request_string = Any( + CharacterString( + ''.join(random.choice(string.lowercase) for _ in range(c_len)) + ) + ) + else: + request_string = None + + # response string is stuffed into the server + if s_len: + anet.iut.confirmed_private_result = Any( + CharacterString( + ''.join(random.choice(string.lowercase) for _ in range(s_len)) + ) + ) + else: + anet.iut.confirmed_private_result = None + + # send the request, get it rejected + s761 = anet.td.start_state.doc("7-6-0") \ + .send(ConfirmedPrivateTransferRequest( + vendorID=999, serviceNumber=1, + serviceParameters=request_string, + destination=anet.iut.address, + )).doc("7-6-1") + + s761.receive(ConfirmedPrivateTransferACK).doc("7-6-2") \ + .success() + s761.receive(AbortPDU, apduAbortRejectReason=11).doc("7-6-3") \ + .success() + + # no IUT application layer matching + anet.iut.start_state.success() + + # run the group + anet.run() + diff --git a/tests/test_service/helpers.py b/tests/test_service/helpers.py index 0e1768c4..5aca62d6 100644 --- a/tests/test_service/helpers.py +++ b/tests/test_service/helpers.py @@ -52,7 +52,7 @@ def __init__(self): ) # test device - self.td = ApplicationNode(self.td_device_object, self.vlan) + self.td = ApplicationStateMachine(self.td_device_object, self.vlan) self.append(self.td) # implementation under test device object @@ -65,7 +65,7 @@ def __init__(self): ) # implementation under test - self.iut = ApplicationNode(self.iut_device_object, self.vlan) + self.iut = ApplicationStateMachine(self.iut_device_object, self.vlan) self.append(self.iut) def run(self, time_limit=60.0): @@ -86,6 +86,7 @@ def run(self, time_limit=60.0): # check for success all_success, some_failed = super(ApplicationNetwork, self).check_for_success() + ApplicationNetwork._debug(" - all_success, some_failed: %r, %r", all_success, some_failed) assert all_success @@ -126,18 +127,18 @@ def confirmation(self, pdu): # -# ApplicationNode +# ApplicationStateMachine # @bacpypes_debugging -class ApplicationNode(Application, StateMachine): +class ApplicationStateMachine(Application, StateMachine): def __init__(self, localDevice, vlan): - if _debug: ApplicationNode._debug("__init__ %r %r", localDevice, vlan) + if _debug: ApplicationStateMachine._debug("__init__ %r %r", localDevice, vlan) # build an address and save it self.address = Address(localDevice.objectIdentifier[1]) - if _debug: ApplicationNode._debug(" - address: %r", self.address) + if _debug: ApplicationStateMachine._debug(" - address: %r", self.address) # continue with initialization Application.__init__(self, localDevice, self.address) @@ -171,22 +172,22 @@ def __init__(self, localDevice, vlan): self.nsap.bind(self.node) def send(self, apdu): - if _debug: ApplicationNode._debug("send(%s) %r", self.name, apdu) + if _debug: ApplicationStateMachine._debug("send(%s) %r", self.name, apdu) # send the apdu down the stack self.request(apdu) def indication(self, apdu): - if _debug: ApplicationNode._debug("indication(%s) %r", self.name, apdu) + if _debug: ApplicationStateMachine._debug("indication(%s) %r", self.name, apdu) # let the state machine know the request was received self.receive(apdu) # allow the application to process it - super(ApplicationNode, self).indication(apdu) + super(ApplicationStateMachine, self).indication(apdu) def confirmation(self, apdu): - if _debug: ApplicationNode._debug("confirmation(%s) %r", self.name, apdu) + if _debug: ApplicationStateMachine._debug("confirmation(%s) %r", self.name, apdu) # forward the confirmation to the state machine self.receive(apdu) diff --git a/tests/test_service/test_device.py b/tests/test_service/test_device.py index aa7fdac5..995b9e8d 100644 --- a/tests/test_service/test_device.py +++ b/tests/test_service/test_device.py @@ -8,14 +8,16 @@ import unittest -from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob +from bacpypes.debugging import bacpypes_debugging, ModuleLogger -from bacpypes.pdu import Address, LocalBroadcast, PDU +from bacpypes.pdu import Address, PDU +from bacpypes.basetypes import PropertyReference from bacpypes.apdu import ( + ConfirmedRequestSequence, SequenceOf, Element, WhoIsRequest, IAmRequest, - WhoHasRequest, WhoHasLimits, WhoHasObject, IHaveRequest, - DeviceCommunicationControlRequest, - SimpleAckPDU, Error, + WhoHasRequest, WhoHasObject, IHaveRequest, + DeviceCommunicationControlRequest, ReadPropertyRequest, + SimpleAckPDU, Error, RejectPDU, AbortPDU, ) from bacpypes.service.device import ( @@ -23,7 +25,7 @@ DeviceCommunicationControlServices, ) -from .helpers import ApplicationNetwork, ApplicationNode +from .helpers import ApplicationNetwork, SnifferNode # some debugging _debug = 0 @@ -391,3 +393,116 @@ def test_incorrect_password(self): # run the group anet.run() + +@bacpypes_debugging +class TestUnrecognizedService(unittest.TestCase): + + class ReadPropertyConditionalRequest(ConfirmedRequestSequence): + serviceChoice = 13 + sequenceElements = [ + # Element('objectSelectionCriteria', ObjectSelectionCriteria, 1), + Element('listOfPropertyReferences', SequenceOf(PropertyReference), 1), + ] + + def test_9_39_1(self): + """9.39.1 Unsupported Confirmed Services Test""" + if _debug: TestUnrecognizedService._debug("test_9_39_1") + + # create a network + anet = ApplicationNetwork() + + # send the request, get it rejected + anet.td.start_state.doc("7-6-0") \ + .send(TestUnrecognizedService.ReadPropertyConditionalRequest( + destination=anet.iut.address, + listOfPropertyReferences=[], + )).doc("7-6-1") \ + .receive(RejectPDU, pduSource=anet.iut.address, apduAbortRejectReason=9).doc("7-6-2") \ + .success() + + # no IUT application layer matching + anet.iut.start_state.success() + + # run the group + anet.run() + + +@bacpypes_debugging +class TestAPDURetryTimeout(unittest.TestCase): + + def test_apdu_retry_default(self): + """Confirmed Request - No Reply""" + if _debug: TestAPDURetryTimeout._debug("test_apdu_retry") + + # create a network + anet = ApplicationNetwork() + + # adjust test if default retries changes + assert anet.iut_device_object.numberOfApduRetries == 3 + + # add a sniffer to see requests without doing anything + sniffer = SnifferNode(anet.vlan) + anet.append(sniffer) + + # no TD application layer matching + anet.td.start_state.success() + + # send a request to a non-existent device, get it rejected + anet.iut.start_state.doc("7-7-0") \ + .send(ReadPropertyRequest( + objectIdentifier=('analogValue', 1), + propertyIdentifier='presentValue', + destination=Address(99), + )).doc("7-7-1") \ + .receive(AbortPDU, apduAbortRejectReason=65).doc("7-7-2") \ + .success() + + # see the attempts and nothing else + sniffer.start_state.doc("7-8-0") \ + .receive(PDU).doc("7-8-1") \ + .receive(PDU).doc("7-8-2") \ + .receive(PDU).doc("7-8-3") \ + .receive(PDU).doc("7-8-4") \ + .timeout(10).doc("7-8-5") \ + .success() + + # run the group + anet.run() + + def test_apdu_retry_1(self): + """Confirmed Request - No Reply""" + if _debug: TestAPDURetryTimeout._debug("test_apdu_retry_1") + + # create a network + anet = ApplicationNetwork() + + # change the retry count in the device properties + anet.iut_device_object.numberOfApduRetries = 1 + + # add a sniffer to see requests without doing anything + sniffer = SnifferNode(anet.vlan) + anet.append(sniffer) + + # no TD application layer matching + anet.td.start_state.success() + + # send a request to a non-existent device, get it rejected + anet.iut.start_state.doc("7-9-0") \ + .send(ReadPropertyRequest( + objectIdentifier=('analogValue', 1), + propertyIdentifier='presentValue', + destination=Address(99), + )).doc("7-9-1") \ + .receive(AbortPDU, apduAbortRejectReason=65).doc("7-9-2") \ + .success() + + # see the attempts and nothing else + sniffer.start_state.doc("7-10-0") \ + .receive(PDU).doc("7-10-1") \ + .receive(PDU).doc("7-10-2") \ + .timeout(10).doc("7-10-3") \ + .success() + + # run the group + anet.run() + diff --git a/tests/test_vlan/test_ipnetwork.py b/tests/test_vlan/test_ipnetwork.py index a825f98e..82f92eb9 100644 --- a/tests/test_vlan/test_ipnetwork.py +++ b/tests/test_vlan/test_ipnetwork.py @@ -346,6 +346,30 @@ def test_send_receive(self): csm_10_3.start_state.timeout(1).success() csm_20_2.start_state.timeout(1).success() + def test_local_broadcast(self): + """Test that a node can send a message to all of the other nodes on + the same network. + """ + if _debug: TestRouter._debug("test_local_broadcast") + + # unpack the state machines + csm_10_2, csm_10_3, csm_20_2, csm_20_3 = self.smg.state_machines + + # make a broadcast PDU from network 10 node 1 + pdu = PDU(b'data', + source=('192.168.10.2', 47808), + destination=('192.168.10.255', 47808), + ) + if _debug: TestVLAN._debug(" - pdu: %r", pdu) + + # node 10-2 sends the pdu, node 10-3 gets pdu, nodes 20-2 and 20-3 dont + csm_10_2.start_state.send(pdu).success() + csm_10_3.start_state.receive(PDU, + pduSource=('192.168.10.2', 47808), + ).success() + csm_20_2.start_state.timeout(1).success() + csm_20_3.start_state.timeout(1).success() + def test_remote_broadcast(self): """Test that a node can send a message to all of the other nodes on a different network. diff --git a/tests/time_machine.py b/tests/time_machine.py index f6a93238..6ea853ae 100755 --- a/tests/time_machine.py +++ b/tests/time_machine.py @@ -90,7 +90,7 @@ def more_to_do(self): return False # peek at the next task and see when it is supposed to run - when, task = self.tasks[0] + when, n, task = self.tasks[0] if when >= self.time_limit: if _debug: TimeMachine._debug(" - next task at or exceeds time limit") return False @@ -119,7 +119,7 @@ def get_next_task(self): else: # peek at the next task and see when it is supposed to run - when, _ = self.tasks[0] + when, n, _ = self.tasks[0] if when >= self.time_limit: if _debug: TimeMachine._debug(" - time limit reached") @@ -128,7 +128,7 @@ def get_next_task(self): else: # pull it off the list - when, task = heappop(self.tasks) + when, n, task = heappop(self.tasks) if _debug: TimeMachine._debug(" - when, task: %r, %s", when, task) # mark that it is no longer scheduled