diff --git a/doc/source/gettingstarted/gettingstarted001.rst b/doc/source/gettingstarted/gettingstarted001.rst index c88263a3..b48997ba 100644 --- a/doc/source/gettingstarted/gettingstarted001.rst +++ b/doc/source/gettingstarted/gettingstarted001.rst @@ -134,10 +134,10 @@ Updating the INI File Now that you know what these values are going to be, you can configure the BACnet portion of your workstation. Change into the -samples directory that you checked out earlier, make a copy +bacpypes directory that you checked out earlier, make a copy of the sample configuration file, and edit it for your site:: - $ cd bacpypes/samples + $ cd bacpypes $ cp BACpypes~.ini BACpypes.ini .. tip:: diff --git a/doc/source/modules/npdu.rst b/doc/source/modules/npdu.rst index 592fb866..38a09055 100644 --- a/doc/source/modules/npdu.rst +++ b/doc/source/modules/npdu.rst @@ -12,35 +12,35 @@ PDU Base Types .. class:: NPCI(PCI) - This is a long line of text. + Header of the network layer message. .. attribute:: npduVersion - This is a long line of text. + This is the version number of the BACnet protocol used. Current version is (1). .. attribute:: npduControl - This is a long line of text. + This is the a single octet. Each bit of the byte indicates the presence of specific fields in the NPCI. .. attribute:: npduDADR - This is a long line of text. + This is the destination address of the network layer message. .. attribute:: npduSADR - This is a long line of text. + This is the source address of the network layer message. .. attribute:: npduHopCount - This is a long line of text. + This is used to determine if network layer messages are being routed in a circular path. .. attribute:: npduNetMessage - This is a long line of text. + This is the network layer message type. .. attribute:: npduVendorID - This is a long line of text. + This is vendor specific ID number used for vendor specific network layer message. .. method:: update(npci) @@ -69,7 +69,7 @@ Service Requests .. class:: WhoIsRouterToNetwork(NPCI) - This is a long line of text. + This message is used to find the router that is the destination for a specific network. It is also used for routers to update routing tables. .. method:: encode(npdu) decode(npdu) @@ -80,7 +80,7 @@ Service Requests .. class:: IAmRouterToNetwork(NPCI) - This is a long line of text. + Response to a WhoIsRouterToNetwork request. Contains network numbers of the networks a router provides access to. .. method:: encode(npdu) decode(npdu) @@ -91,7 +91,7 @@ Service Requests .. class:: ICouldBeRouterToNetwork(NPCI) - This is a long line of text. + Response to a WhoIsRouterToNetwork request. Contains network numbers of the networks a half-router could provide access to over a PTP connection, but the connection is not currently established. .. method:: encode(npdu) decode(npdu) @@ -102,7 +102,7 @@ Service Requests .. class:: RejectMessageToNetwork(NPCI) - This is a long line of text. + This is a message sent in response to a network layer message that was rejected due to an error. .. method:: encode(npdu) decode(npdu) @@ -113,7 +113,7 @@ Service Requests .. class:: RouterBusyToNetwork(NPCI) - This is a long line of text. + This is a message sent by a router to temporarily stop messages to specific destination networks. .. method:: encode(npdu) decode(npdu) @@ -124,7 +124,7 @@ Service Requests .. class:: RouterAvailableToNetwork(NPCI) - This is a long line of text. + This is a message sent by a router to enable or re-enable messages to specific destination networks. .. method:: encode(npdu) decode(npdu) @@ -151,7 +151,7 @@ Service Requests .. class:: InitializeRoutingTable(NPCI) - This is a long line of text. + This is a message used to initialize the routing table of a router or get the contents of the current routing table. .. method:: encode(npdu) decode(npdu) @@ -162,7 +162,7 @@ Service Requests .. class:: InitializeRoutingTableAck(NPCI) - This is a long line of text. + This is a message indicating the routing table of a router has been changed or the routing table has been initialized. .. method:: encode(npdu) decode(npdu) @@ -173,7 +173,7 @@ Service Requests .. class:: EstablishConnectionToNetwork(NPCI) - This is a long line of text. + This is a message used to tell a half-router to make a PTP connection to a network. .. method:: encode(npdu) decode(npdu) @@ -184,7 +184,7 @@ Service Requests .. class:: DisconnectConnectionToNetwork(NPCI) - This is a long line of text. + This is a message used to tell a half-router to close a PTP connection to a network. .. method:: encode(npdu) decode(npdu) diff --git a/py25/bacpypes/__init__.py b/py25/bacpypes/__init__.py index e99ac48b..e4bed942 100755 --- a/py25/bacpypes/__init__.py +++ b/py25/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.17.5' +__version__ = '0.17.6' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py25/bacpypes/analysis.py b/py25/bacpypes/analysis.py index 1a10b494..aaba96b7 100755 --- a/py25/bacpypes/analysis.py +++ b/py25/bacpypes/analysis.py @@ -194,9 +194,13 @@ def decode_packet(data): if (pdu.pduData[0] == '\x81'): if _debug: decode_packet._debug(" - BVLL header found") - xpdu = BVLPDU() - xpdu.decode(pdu) - pdu = xpdu + try: + xpdu = BVLPDU() + xpdu.decode(pdu) + pdu = xpdu + except Exception as err: + if _debug: decode_packet._debug(" - BVLPDU decoding error: %r", err) + return pdu # make a more focused interpretation atype = bvl_pdu_types.get(pdu.bvlciFunction) diff --git a/py25/bacpypes/apdu.py b/py25/bacpypes/apdu.py index 118b4ab0..4fa8279f 100755 --- a/py25/bacpypes/apdu.py +++ b/py25/bacpypes/apdu.py @@ -76,7 +76,7 @@ def encode_max_segments_accepted(arg): if _max_segments_accepted_encoding[i] <= arg: return i - raise ValueError("invalid max max segments accepted: {0}".format(arg)) + raise ValueError("invalid max max segments accepted: %r" % (arg,)) def decode_max_segments_accepted(arg): """Decode the maximum number of segments the device will accept, Section @@ -97,12 +97,12 @@ def encode_max_apdu_length_accepted(arg): if (arg >= _max_apdu_length_encoding[i]): return i - raise ValueError("invalid max APDU length accepted: {0}".format(arg)) + raise ValueError("invalid max APDU length accepted: %r" % (arg,)) def decode_max_apdu_length_accepted(arg): v = _max_apdu_length_encoding[arg] if not v: - raise ValueError("invalid max APDU length accepted: {0}".format(arg)) + raise ValueError("invalid max APDU length accepted: %r" % (arg,)) return v @@ -927,7 +927,7 @@ class RangeByPosition(Sequence): class RangeBySequenceNumber(Sequence): sequenceElements = \ - [ Element('referenceIndex', Unsigned) + [ Element('referenceSequenceNumber', Unsigned) , Element('count', Integer) ] diff --git a/py25/bacpypes/app.py b/py25/bacpypes/app.py index 9be79fb7..92f3070a 100755 --- a/py25/bacpypes/app.py +++ b/py25/bacpypes/app.py @@ -598,7 +598,7 @@ def close_socket(self): class BIPNetworkApplication(NetworkServiceElement): - def __init__(self, localAddress, eID=None): + def __init__(self, localAddress, bbmdAddress=None, bbmdTTL=None, eID=None): if _debug: BIPNetworkApplication._debug("__init__ %r eID=%r", localAddress, eID) NetworkServiceElement.__init__(self, eID) @@ -616,7 +616,10 @@ def __init__(self, localAddress, eID=None): # create a generic BIP stack, bound to the Annex J server # on the UDP multiplexer - self.bip = BIPSimple() + if (not bbmdAddress) and (not bbmdTTL): + self.bip = BIPSimple() + else: + self.bip = BIPForeign(bbmdAddress, bbmdTTL) self.annexj = AnnexJCodec() self.mux = UDPMultiplexer(self.localAddress) diff --git a/py25/bacpypes/appservice.py b/py25/bacpypes/appservice.py index b411c02a..89691b9b 100755 --- a/py25/bacpypes/appservice.py +++ b/py25/bacpypes/appservice.py @@ -49,7 +49,7 @@ class SSM(OneShotTask, DebugContents): _debug_contents = ('ssmSAP', 'localDevice', 'device_info', 'invokeID' , 'state', 'segmentAPDU', 'segmentSize', 'segmentCount', 'maxSegmentsAccepted' , 'retryCount', 'segmentRetryCount', 'sentAllSegments', 'lastSequenceNumber' - , 'initialSequenceNumber', 'actualWindowSize', 'proposedWindowSize' + , 'initialSequenceNumber', 'actualWindowSize' ) def __init__(self, sap, pdu_address): @@ -157,7 +157,7 @@ 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") @@ -193,8 +193,8 @@ def get_segment(self, indx): # first segment sends proposed window size, rest get actual if indx == 0: - if _debug: SSM._debug(" - proposedWindowSize: %r", self.proposedWindowSize) - segAPDU.apduWin = self.proposedWindowSize + if _debug: SSM._debug(" - proposedWindowSize: %r", self.ssmSAP.proposedWindowSize) + segAPDU.apduWin = self.ssmSAP.proposedWindowSize else: if _debug: SSM._debug(" - actualWindowSize: %r", self.actualWindowSize) segAPDU.apduWin = self.actualWindowSize @@ -506,7 +506,7 @@ def segmented_request(self, apdu): self.set_segmentation_context(apdu) # minimum of what the server is proposing and this client proposes - self.actualWindowSize = min(apdu.apduWin, self.proposedWindowSize) + self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.proposedWindowSize) self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 self.set_state(SEGMENTED_CONFIRMATION, self.segmentTimeout) @@ -825,15 +825,15 @@ def confirmation(self, apdu): return # make sure client supports segmented receive - if self.device_info.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if not self.segmented_response_accepted: if _debug: ServerSSM._debug(" - client can't receive segmented responses") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return # 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: + # that the client said it was willing to accept in the request + if (self.maxSegmentsAccepted is not None) and (self.segmentCount > self.maxSegmentsAccepted): if _debug: ServerSSM._debug(" - client can't receive enough segments") abort = self.abort(AbortReason.apduTooLong) self.response(abort) @@ -898,11 +898,12 @@ def idle(self, apdu): self.invokeID = apdu.apduInvokeID if _debug: ServerSSM._debug(" - invoke ID: %r", self.invokeID) - if apdu.apduSA: - if not self.device_info: - if _debug: ServerSSM._debug(" - no client device info") + # remember if the client accepts segmented responses + self.segmented_response_accepted = apdu.apduSA - elif self.device_info.segmentationSupported == 'noSegmentation': + # if there is a cache record, check to see if it needs to be updated + if apdu.apduSA and self.device_info: + if self.device_info.segmentationSupported == 'noSegmentation': if _debug: ServerSSM._debug(" - client actually supports segmented receive") self.device_info.segmentationSupported = 'segmentedReceive' @@ -958,8 +959,11 @@ def idle(self, apdu): # 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) + self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.proposedWindowSize) + if _debug: ServerSSM._debug( + " - actualWindowSize? min(%r, %r) -> %r", + apdu.apduWin, self.ssmSAP.proposedWindowSize, self.actualWindowSize, + ) # initialize the state self.lastSequenceNumber = 0 diff --git a/py25/bacpypes/basetypes.py b/py25/bacpypes/basetypes.py index dcbd5ed8..6e5b75e5 100755 --- a/py25/bacpypes/basetypes.py +++ b/py25/bacpypes/basetypes.py @@ -8,7 +8,7 @@ from .primitivedata import BitString, Boolean, CharacterString, Date, Double, \ Enumerated, Integer, Null, ObjectIdentifier, OctetString, Real, Time, \ - Unsigned + Unsigned, Unsigned16 from .constructeddata import Any, AnyAtomic, ArrayOf, Choice, Element, \ Sequence, SequenceOf @@ -94,6 +94,7 @@ class ObjectTypesSupported(BitString): , 'accessUser':35 , 'accessZone':36 , 'credentialDataInput':37 + , 'networkPort':56 , 'networkSecurity':38 , 'bitstringValue':39 , 'characterstringValue':40 @@ -1101,9 +1102,50 @@ class ProgramState(Enumerated): } class PropertyIdentifier(Enumerated): + # TODO: Sort Alphabetically vendor_range = (512, 4194303) enumerations = \ { 'absenteeLimit':244 + , 'tags':486 + , 'profileLocation':91 + , 'eventDetectionEnabled':353 + , 'apduLength':388 + , 'linkSpeed':420 + , 'linkSpeeds':421 + , 'linkSpeedAutonegotiate':422 + , 'networkInterfaceName':424 + , 'bacnetIPMode':408 + , 'ipAddress':400 + , 'bacnetIPUDPPort':412 + , 'ipSubnetMask':411 + , 'ipDefaultGateway':401 + , 'bacnetIPMulticastAddress':409 + , 'ipDNSServer':406 + , 'ipDHCPEnable':402 + , 'ipDHCPLeaseTime':403 + , 'ipDHCPLeaseTimeRemaining':404 + , 'ipDHCPServer':405 + , 'bacnetIPNATTraversal':410 + , 'bacnetIPGlobalAddress':407 + , 'bbmdBroadcastDistributionTable':414 + , 'bbmdAcceptFDRegistrations':413 + , 'bbmdForeignDeviceTable':415 + , 'fdBBMDAddress':418 + , 'fdSubscriptionLifetime':419 + , 'bacnetIPv6Mode':435 + , 'ipv6Address':436 + , 'ipv6PrefixLength':437 + , 'bacnetIPv6UDPPort':438 + , 'ipv6DefaultGateway':439 + , 'bacnetIPv6MulticastAddress':440 + , 'ipv6DNSServer':441 + , 'ipv6AutoAddressingEnabled':442 + , 'ipv6DHCPLeaseTime':443 + , 'ipv6DHCPLeaseTimeRemaining':444 + , 'ipv6DHCPServer':445 + , 'ipv6ZoneIndex':446 + , 'virtualMACAddressTable':429 + , 'routingTable':428 , 'acceptedModes':175 , 'accessAlarmEvents':245 , 'accessDoors':246 @@ -1158,8 +1200,10 @@ class PropertyIdentifier(Enumerated): , 'bufferSize':126 , 'changeOfStateCount':15 , 'changeOfStateTime':16 + , 'changesPending':416 , 'channelNumber':366 , 'clientCovIncrement':127 + , 'command':417 , 'configurationFiles':154 , 'controlGroups':367 , 'controlledVariableReference':19 @@ -1286,6 +1330,7 @@ class PropertyIdentifier(Enumerated): , 'loggingRecord':184 , 'loggingType':197 , 'lowLimit':59 + , 'macAddress':423 , 'maintenanceRequired':158 , 'manipulatedVariableReference':60 , 'manualSlaveAddressBinding':170 @@ -1317,6 +1362,9 @@ class PropertyIdentifier(Enumerated): , 'musterPoint':287 , 'negativeAccessRules':288 , 'networkAccessSecurityPolicies':332 + , 'networkNumber':425 + , 'networkNumberQuality':427 + , 'networkType': 427 , 'nodeSubtype':207 , 'nodeType':208 , 'notificationClass':17 @@ -1365,6 +1413,7 @@ class PropertyIdentifier(Enumerated): , 'propertyList':371 , 'proportionalConstant':93 , 'proportionalConstantUnits':94 + , 'protocolLevel':482 , 'protocolObjectTypesSupported':96 , 'protocolRevision':139 , 'protocolServicesSupported':97 @@ -1376,6 +1425,7 @@ class PropertyIdentifier(Enumerated): , 'recipientList':102 , 'recordsSinceNotification':140 , 'recordCount':141 + , 'referencePort':483 , 'reliability':103 , 'reliabilityEvaluationInhibit':357 , 'relinquishDefault':104 @@ -1566,10 +1616,112 @@ class WriteStatus(Enumerated): , 'failed':3 } +class NetworkType(Enumerated): + enumerations = \ + { 'ethernet':0 + , 'arcnet':1 + , 'mstp':2 + , 'ptp':3 + , 'lontalk':4 + , 'ipv4':5 + , 'zigbee':6 + , 'virtual': 7 + # , 'non-bacnet': 8 Removed in Version 1, Revision 18 + , 'ipv6':9 + , 'serial':10 + } + +class ProtocolLevel(Enumerated): + enumerations = \ + { 'physical':0 + , 'protocol':1 + , 'bacnetApplication':2 + , 'nonBacnetApplication':3 + } + +class NetworkNumberQuality(Enumerated): + enumerations = \ + { 'unknown':0 + , 'learned':1 + , 'learnedConfigured':2 + , 'configured':3 + } + +class NetworkPortCommand(Enumerated): + enumerations = \ + { 'idle':0 + , 'discardChanges':1 + , 'renewFdDRegistration':2 + , 'restartSlaveDiscovery':3 + , 'renewDHCP':4 + , 'restartAutonegotiation':5 + , 'disconnect':6 + , 'restartPort':7 + } + +class IPMode(Enumerated): + enumerations = \ + { 'normal':0 + , 'foreign':1 + , 'bbmd':2 + } + +class RouterEntryStatus(Enumerated): + enumerations = \ + { 'available':0 + , 'busy':1 + , 'disconnected':2 + } + # # Forward Sequences # +class HostAddress(Choice): + choiceElements = \ + [ Element('none', Null) + , Element('ipAddress', OctetString) # 4 octets for B/IP or 16 octets for B/IPv6 + , Element('name', CharacterString) # Internet host name (see RFC 1123) + ] + +class HostNPort(Sequence): + sequenceElements = \ + [ Element('host', HostAddress) + , Element('port', Unsigned16) + ] + +class BDTEntry(Sequence): + sequenceElements = \ + [ Element('bbmdAddress', HostNPort) + , Element('broadcastMask', OctetString) # shall be present if BACnet/IP, and absent for BACnet/IPv6 + ] + +class FDTEntry(Sequence): + sequenceElements = \ + [ Element('bacnetIPAddress', OctetString) # the 6-octet B/IP or 18-octet B/IPv6 address of the registrant + , Element('timeToLive', Unsigned16) # time to live in seconds at the time of registration + , Element('remainingTimeToLive', Unsigned16) # remaining time to live in seconds, incl. grace period + ] + +class VMACEntry(Sequence): + sequenceElements = \ + [ Element('virtualMACAddress', OctetString) # maximum size 6 octets + , Element('nativeMACAddress', OctetString) + ] + +class RouterEntry(Sequence): + sequenceElements = \ + [ Element('networkNumber', Unsigned16) + , Element('macAddress', OctetString) + , Element('status', RouterEntryStatus) # Defined Above + ] + +class NameValue(Sequence): + sequenceElements = \ + [ Element('name', CharacterString) + , Element('value', AnyAtomic) # IS ATOMIC CORRECT HERE? value is limited to primitive datatypes and BACnetDateTime + ] + class DeviceAddress(Sequence): sequenceElements = \ [ Element('networkNumber', Unsigned) @@ -1730,7 +1882,8 @@ class AccessRule(Sequence): ] class AccessThreatLevel(Unsigned): - pass + _low_limit = 0 + _high_limit = 100 class AccumulatorRecord(Sequence): sequenceElements = \ diff --git a/py25/bacpypes/bvllservice.py b/py25/bacpypes/bvllservice.py index e90f53b8..b17301d9 100755 --- a/py25/bacpypes/bvllservice.py +++ b/py25/bacpypes/bvllservice.py @@ -15,7 +15,7 @@ from .comm import Client, Server, bind, \ ServiceAccessPoint, ApplicationServiceElement -from .pdu import Address, LocalBroadcast, LocalStation, PDU, \ +from .pdu import Address, LocalBroadcast, PDU, \ unpack_ip_addr from .bvll import BVLPDU, DeleteForeignDeviceTableEntry, \ DistributeBroadcastToNetwork, FDTEntry, ForwardedNPDU, \ @@ -916,7 +916,7 @@ def register_foreign_device(self, addr, ttl): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation( addr ) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") @@ -941,7 +941,7 @@ def delete_foreign_device_table_entry(self, addr): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation( addr ) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") @@ -975,7 +975,7 @@ def add_peer(self, addr): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation(addr) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") @@ -993,7 +993,7 @@ def delete_peer(self, addr): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation(addr) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") @@ -1215,7 +1215,7 @@ def register_foreign_device(self, addr, ttl): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation( addr ) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") @@ -1240,7 +1240,7 @@ def delete_foreign_device_table_entry(self, addr): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation( addr ) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") @@ -1274,7 +1274,7 @@ def add_peer(self, addr): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation(addr) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") @@ -1296,7 +1296,7 @@ def delete_peer(self, addr): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation(addr) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") diff --git a/py25/bacpypes/comm.py b/py25/bacpypes/comm.py index 27b7f4e7..765744f7 100755 --- a/py25/bacpypes/comm.py +++ b/py25/bacpypes/comm.py @@ -419,7 +419,9 @@ def __init__(self, **terminals): Server.__init__(self) # wrap the terminals - self.terminals = {k:Switch.TerminalWrapper(self, v) for k, v in terminals.items()} + self.terminals = {} + for k, v in terminals.items(): + self.terminals[k] = Switch.TerminalWrapper(self, v) self.current_terminal = None def __getitem__(self, key): diff --git a/py25/bacpypes/constructeddata.py b/py25/bacpypes/constructeddata.py index 6d04f93a..d483cfca 100755 --- a/py25/bacpypes/constructeddata.py +++ b/py25/bacpypes/constructeddata.py @@ -7,7 +7,7 @@ import sys from copy import deepcopy as _deepcopy -from .errors import DecodingError, \ +from .errors import DecodingError, EncodingError, \ MissingRequiredParameter, InvalidParameterDatatype, InvalidTag from .debugging import ModuleLogger, bacpypes_debugging @@ -1461,3 +1461,47 @@ def __repr__(self): bacpypes_debugging(AnyAtomic) +# +# SequenceOfAny +# + +class SequenceOfAny(Any): + + def cast_in(self, element): + """encode the element into the internal tag list.""" + if _debug: SequenceOfAny._debug("cast_in %r", element) + + # make sure it is a list + if not isinstance(element, List): + raise EncodingError("%r is not a list" % (element,)) + + t = TagList() + element.encode(t) + + self.tagList.extend(t.tagList) + + def cast_out(self, klass): + """Interpret the content as a particular class.""" + if _debug: SequenceOfAny._debug("cast_out %r", klass) + + # make sure it is a list + if not issubclass(klass, List): + raise DecodingError("%r is not a list" % (klass,)) + + # build a helper + helper = klass() + + # make a copy of the tag list + t = TagList(self.tagList[:]) + + # let it decode itself + helper.decode(t) + + # make sure everything was consumed + if len(t) != 0: + raise DecodingError("incomplete cast") + + return helper.value + +bacpypes_debugging(AnyAtomic) + diff --git a/py25/bacpypes/debugging.py b/py25/bacpypes/debugging.py index 5c62e6b2..a3a8f72b 100755 --- a/py25/bacpypes/debugging.py +++ b/py25/bacpypes/debugging.py @@ -10,20 +10,8 @@ import binascii from cStringIO import StringIO - -# set the level of the root logger -_root = logging.getLogger() -_root.setLevel(1) - -# add a stream handler for warnings and up -hdlr = logging.StreamHandler() -if ('--debugDebugging' in sys.argv): - hdlr.setLevel(logging.DEBUG) -else: - hdlr.setLevel(logging.WARNING) -hdlr.setFormatter(logging.Formatter(logging.BASIC_FORMAT, None)) -_root.addHandler(hdlr) -del hdlr +# create a root logger +root_logger = logging.getLogger('bacpypes') def btox(data, sep=''): @@ -68,12 +56,22 @@ def ModuleLogger(globs): if not globs.has_key('_debug'): raise RuntimeError("define _debug before creating a module logger") + # logger name is the module name + logger_name = globs['__name__'] + # create a logger to be assigned to _log - logger = logging.getLogger(globs['__name__']) + logger = logging.getLogger(logger_name) # put in a reference to the module globals logger.globs = globs + # if this is a "root" logger add a default handler for warnings and up + if '.' not in logger_name: + hdlr = logging.StreamHandler() + hdlr.setLevel(logging.WARNING) + hdlr.setFormatter(logging.Formatter(logging.BASIC_FORMAT, None)) + logger.addHandler(hdlr) + return logger # diff --git a/py25/bacpypes/object.py b/py25/bacpypes/object.py index 297d035c..0254175a 100755 --- a/py25/bacpypes/object.py +++ b/py25/bacpypes/object.py @@ -14,7 +14,7 @@ from .primitivedata import Atomic, BitString, Boolean, CharacterString, Date, \ Double, Integer, ObjectIdentifier, ObjectType, OctetString, Real, Time, \ - Unsigned + Unsigned, Unsigned8, Unsigned16 from .constructeddata import AnyAtomic, Array, ArrayOf, List, ListOf, \ Choice, Element, Sequence from .basetypes import AccessCredentialDisable, AccessCredentialDisableReason, \ @@ -22,24 +22,26 @@ AccessUserType, AccessZoneOccupancyState, AccumulatorRecord, Action, \ ActionList, AddressBinding, AssignedAccessRights, AuthenticationFactor, \ AuthenticationFactorFormat, AuthenticationPolicy, AuthenticationStatus, \ - AuthorizationException, AuthorizationMode, BackupState, BinaryPV, \ + AuthorizationException, AuthorizationMode, BackupState, BDTEntry, BinaryPV, \ COVSubscription, CalendarEntry, ChannelValue, ClientCOV, \ CredentialAuthenticationFactor, DailySchedule, DateRange, DateTime, \ Destination, DeviceObjectPropertyReference, DeviceObjectReference, \ DeviceStatus, DoorAlarmState, DoorSecuredStatus, DoorStatus, DoorValue, \ EngineeringUnits, EventNotificationSubscription, EventParameter, \ EventState, EventTransitionBits, EventType, FaultParameter, FaultType, \ - FileAccessMethod, LifeSafetyMode, LifeSafetyOperation, LifeSafetyState, \ + FileAccessMethod, FDTEntry, IPMode, HostNPort, LifeSafetyMode, LifeSafetyOperation, LifeSafetyState, \ LightingCommand, LightingInProgress, LightingTransition, LimitEnable, \ LockStatus, LogMultipleRecord, LogRecord, LogStatus, LoggingType, \ - Maintenance, NetworkSecurityPolicy, NodeType, NotifyType, \ + Maintenance, NameValue, NetworkNumberQuality, NetworkPortCommand, \ + NetworkSecurityPolicy, NetworkType, NodeType, NotifyType, \ ObjectPropertyReference, ObjectTypesSupported, OptionalCharacterString, \ Polarity, PortPermission, Prescale, PriorityArray, ProcessIdSelection, \ ProgramError, ProgramRequest, ProgramState, PropertyAccessResult, \ - PropertyIdentifier, Recipient, Reliability, RestartReason, Scale, \ - SecurityKeySet, SecurityLevel, Segmentation, ServicesSupported, \ - SetpointReference, ShedLevel, ShedState, SilencedState, SpecialEvent, \ - StatusFlags, TimeStamp, VTClass, VTSession, WriteStatus + PropertyIdentifier, ProtocolLevel, Recipient, Reliability, RestartReason, \ + RouterEntry, Scale, SecurityKeySet, SecurityLevel, Segmentation, \ + ServicesSupported, SetpointReference, ShedLevel, ShedState, SilencedState, \ + SpecialEvent, StatusFlags, TimeStamp, VTClass, VTSession, VMACEntry, \ + WriteStatus from .apdu import EventNotificationParameters, ReadAccessSpecification, \ ReadAccessResult @@ -1913,6 +1915,82 @@ class MultiStateValueObject(Object): register_object_type(MultiStateValueObject) +class NetworkPortObject(Object): + objectType = 'networkPort' #56 + properties = \ + [ ReadableProperty('statusFlags', StatusFlags) #111 + , ReadableProperty('reliability', Reliability) #103 + , ReadableProperty('outOfService', Boolean) #81 + , ReadableProperty('networkType', NetworkType) #427 + , ReadableProperty('protocolLevel', ProtocolLevel) #482 + , OptionalProperty('referencePort', Unsigned) #483 + , ReadableProperty('networkNumber', Unsigned16) #425 + , ReadableProperty('networkNumberQuality', NetworkNumberQuality) #427 + , ReadableProperty('changesPending', Boolean) #416 + , OptionalProperty('command', NetworkPortCommand) #417 + , OptionalProperty('macAddress', OctetString) #423 + , ReadableProperty('apduLength', Unsigned) #388 + , ReadableProperty('linkSpeed', Real) #420 + , OptionalProperty('linkSpeeds', ArrayOf(Real)) #421 + , OptionalProperty('eventTimeStamps', ArrayOf(TimeStamp)) #130 + , OptionalProperty('linkSpeedAutonegotiate', Boolean) #422 + , OptionalProperty('networkInterfaceName', CharacterString) #424 + , OptionalProperty('bacnetIPMode', IPMode) #408 + , OptionalProperty('ipAddress', OctetString) #400 + , OptionalProperty('bacnetIPUDPPort', Unsigned16) #412 + , OptionalProperty('ipSubnetMask', OctetString) #411 + , OptionalProperty('ipDefaultGateway', OctetString) #401 + , OptionalProperty('bacnetIPMulticastAddress', OctetString) #409 + , OptionalProperty('ipDNSServer', ArrayOf(OctetString)) #406 + , OptionalProperty('ipDHCPEnable', Boolean) #402 + , OptionalProperty('ipDHCPLeaseTime', Unsigned) #403 + , OptionalProperty('ipDHCPLeaseTimeRemaining', Unsigned) #404 + , OptionalProperty('ipDHCPServer', OctetString) #405 + , OptionalProperty('bacnetIPNATTraversal', Boolean) #410 + , OptionalProperty('bacnetIPGlobalAddress', HostNPort) #407 + , OptionalProperty('bbmdBroadcastDistributionTable', ListOf(BDTEntry)) #414 + , OptionalProperty('bbmdAcceptFDRegistrations', Boolean) #413 + , OptionalProperty('bbmdForeignDeviceTable', ListOf(FDTEntry)) #415 + , OptionalProperty('fdBBMDAddress', HostNPort) #418 + , OptionalProperty('fdSubscriptionLifetime', Unsigned16) #419 + , OptionalProperty('bacnetIPv6Mode', IPMode) #435 + , OptionalProperty('ipv6Address', OctetString) #436 + , OptionalProperty('ipv6PrefixLength', Unsigned8) #437 + , OptionalProperty('bacnetIPv6UDPPort', Unsigned16) #438 + , OptionalProperty('ipv6DefaultGateway', OctetString) #439 + , OptionalProperty('bacnetIPv6MulticastAddress', OctetString) #440 + , OptionalProperty('ipv6DNSServer', OctetString) #441 + , OptionalProperty('ipv6AutoAddressingEnabled', Boolean) #442 + , OptionalProperty('ipv6DHCPLeaseTime', Unsigned) #443 + , OptionalProperty('ipv6DHCPLeaseTimeRemaining', Unsigned) #444 + , OptionalProperty('ipv6DHCPServer', OctetString) #445 + , OptionalProperty('ipv6ZoneIndex', CharacterString) #446 + , OptionalProperty('maxMaster', Unsigned8) #64 + , OptionalProperty('maxInfoFrames', Unsigned8) #63 + , OptionalProperty('slaveProxyEnable', Boolean) #172 + , OptionalProperty('manualSlaveAddressBinding', ListOf(AddressBinding)) #170 + , OptionalProperty('autoSlaveDiscovery', Boolean) #169 + , OptionalProperty('slaveAddressBinding', ListOf(AddressBinding)) #171 + , OptionalProperty('virtualMACAddressTable', ListOf(VMACEntry)) #429 + , OptionalProperty('routingTable', ListOf(RouterEntry)) #428 + , OptionalProperty('eventDetectionEnabled', Boolean) #353 + , OptionalProperty('notificationClass', Unsigned) #17 + , OptionalProperty('eventEnable', EventTransitionBits) #35 + , OptionalProperty('ackedTransitions', EventTransitionBits) #0 + , OptionalProperty('notifyType', NotifyType) #72 + , OptionalProperty('eventTimeStamps', ArrayOf(TimeStamp, 3)) #130 + , OptionalProperty('eventMessageTexts', ArrayOf(CharacterString, 3)) #351 + , OptionalProperty('eventMessageTextsConfig', ArrayOf(CharacterString, 3)) #352 + , OptionalProperty('eventState', EventState) #36 + , ReadableProperty('reliabilityEvaluationInhibit', Boolean) #357 + , OptionalProperty('propertyList', ArrayOf(PropertyIdentifier)) #371 + , OptionalProperty('tags', ArrayOf(NameValue)) #486 + , OptionalProperty('profileLocation', CharacterString) #91 + , OptionalProperty('profileName', CharacterString) #168 + ] + +register_object_type(NetworkPortObject) + class NetworkSecurityObject(Object): objectType = 'networkSecurity' properties = \ diff --git a/py25/bacpypes/primitivedata.py b/py25/bacpypes/primitivedata.py index bf3bf12a..668c99ac 100755 --- a/py25/bacpypes/primitivedata.py +++ b/py25/bacpypes/primitivedata.py @@ -594,8 +594,10 @@ def __str__(self): class Unsigned(Atomic): _app_tag = Tag.unsignedAppTag + _low_limit = 0 + _high_limit = None - def __init__(self,arg = None): + def __init__(self, arg=None): self.value = 0L if arg is None: @@ -603,14 +605,16 @@ def __init__(self,arg = None): elif isinstance(arg, Tag): self.decode(arg) elif isinstance(arg, int): - if (arg < 0): - raise ValueError("unsigned integer required") + if not self.is_valid(arg): + raise ValueError("value out of range") self.value = long(arg) elif isinstance(arg, long): - if (arg < 0): - raise ValueError("unsigned integer required") + if not self.is_valid(arg): + raise ValueError("value out of range") self.value = arg elif isinstance(arg, Unsigned): + if not self.is_valid(arg.value): + raise ValueError("value out of range") self.value = arg.value else: raise TypeError("invalid constructor datatype") @@ -621,7 +625,7 @@ def encode(self, tag): # reduce the value to the smallest number of octets while (len(data) > 1) and (data[0] == '\x00'): - data = data[1:] + del data[0] # encode the tag tag.set_app_data(Tag.unsignedAppTag, data) @@ -643,10 +647,24 @@ def decode(self, tag): @classmethod def is_valid(cls, arg): """Return True if arg is valid value for the class.""" - return isinstance(arg, (int, long)) and (not isinstance(arg, bool)) and (arg >= 0) + if not isinstance(arg, (int, long)) or isinstance(arg, bool): + return False + if (arg < cls._low_limit): + return False + if (cls._high_limit is not None) and (arg > cls._high_limit): + return False + return True def __str__(self): - return "Unsigned(%s)" % (self.value, ) + return "%s(%s)" % (self.__class__.__name__, self.value) + +class Unsigned8(Unsigned): + _low_limit = 0 + _high_limit = 255 + +class Unsigned16(Unsigned): + _low_limit = 0 + _high_limit = 65535 # # Integer diff --git a/py25/bacpypes/service/object.py b/py25/bacpypes/service/object.py index a7fe647a..fc631733 100755 --- a/py25/bacpypes/service/object.py +++ b/py25/bacpypes/service/object.py @@ -69,11 +69,11 @@ def do_ReadPropertyRequest(self, apdu): elif issubclass(datatype.subtype, Atomic): value = datatype.subtype(value) elif not isinstance(value, datatype.subtype): - raise TypeError("invalid result datatype, expecting {0} and got {1}" \ - .format(datatype.subtype.__name__, type(value).__name__)) + raise TypeError("invalid result datatype, expecting %r and got %r" \ + % (datatype.subtype.__name__, type(value).__name__)) elif not isinstance(value, datatype): - raise TypeError("invalid result datatype, expecting {0} and got {1}" \ - .format(datatype.__name__, type(value).__name__)) + raise TypeError("invalid result datatype, expecting %r and got %r" \ + % (datatype.__name__, type(value).__name__)) if _debug: ReadWritePropertyServices._debug(" - encodeable value: %r", value) # this is a ReadProperty ack diff --git a/py27/bacpypes/__init__.py b/py27/bacpypes/__init__.py index e99ac48b..e4bed942 100755 --- a/py27/bacpypes/__init__.py +++ b/py27/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.17.5' +__version__ = '0.17.6' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py27/bacpypes/analysis.py b/py27/bacpypes/analysis.py index 96fbd19a..c13ff455 100755 --- a/py27/bacpypes/analysis.py +++ b/py27/bacpypes/analysis.py @@ -191,9 +191,13 @@ def decode_packet(data): if (pdu.pduData[0] == '\x81'): if _debug: decode_packet._debug(" - BVLL header found") - xpdu = BVLPDU() - xpdu.decode(pdu) - pdu = xpdu + try: + xpdu = BVLPDU() + xpdu.decode(pdu) + pdu = xpdu + except Exception as err: + if _debug: decode_packet._debug(" - BVLPDU decoding error: %r", err) + return pdu # make a more focused interpretation atype = bvl_pdu_types.get(pdu.bvlciFunction) @@ -360,8 +364,12 @@ def decode_file(fname): # loop through the packets for i, (timestamp, data) in enumerate(p): - pkt = decode_packet(data) - if not pkt: + try: + pkt = decode_packet(data) + if not pkt: + continue + except Exception as err: + if _debug: decode_file._debug(" - exception decoding packet %d: %r", i+1, err) continue # save the packet number (as viewed in Wireshark) and timestamp diff --git a/py27/bacpypes/apdu.py b/py27/bacpypes/apdu.py index c19a811e..3062bd71 100755 --- a/py27/bacpypes/apdu.py +++ b/py27/bacpypes/apdu.py @@ -11,7 +11,8 @@ from .primitivedata import Boolean, CharacterString, Enumerated, Integer, \ ObjectIdentifier, ObjectType, OctetString, Real, TagList, Unsigned, \ expand_enumerations -from .constructeddata import Any, Choice, Element, Sequence, SequenceOf +from .constructeddata import Any, Choice, Element, Sequence, SequenceOf, \ + SequenceOfAny from .basetypes import ChannelValue, DateTime, DeviceAddress, ErrorType, \ EventState, EventTransitionBits, EventType, LifeSafetyOperation, \ NotificationParameters, NotifyType, ObjectPropertyReference, \ @@ -920,7 +921,7 @@ class RangeByPosition(Sequence): class RangeBySequenceNumber(Sequence): sequenceElements = \ - [ Element('referenceIndex', Unsigned) + [ Element('referenceSequenceNumber', Unsigned) , Element('count', Integer) ] @@ -956,7 +957,7 @@ class ReadRangeACK(ComplexAckSequence): , Element('propertyArrayIndex', Unsigned, 2, True) , Element('resultFlags', ResultFlags, 3) , Element('itemCount', Unsigned, 4) - , Element('itemData', SequenceOf(Any), 5) + , Element('itemData', SequenceOfAny, 5) , Element('firstSequenceNumber', Unsigned, 6, True) ] diff --git a/py27/bacpypes/app.py b/py27/bacpypes/app.py index ad24d4fc..c3f3426a 100755 --- a/py27/bacpypes/app.py +++ b/py27/bacpypes/app.py @@ -593,7 +593,7 @@ def close_socket(self): @bacpypes_debugging class BIPNetworkApplication(NetworkServiceElement): - def __init__(self, localAddress, eID=None): + def __init__(self, localAddress, bbmdAddress=None, bbmdTTL=None, eID=None): if _debug: BIPNetworkApplication._debug("__init__ %r eID=%r", localAddress, eID) NetworkServiceElement.__init__(self, eID) @@ -611,9 +611,12 @@ def __init__(self, localAddress, eID=None): # create a generic BIP stack, bound to the Annex J server # on the UDP multiplexer - self.bip = BIPSimple() + if (not bbmdAddress) and (not bbmdTTL): + self.bip = BIPSimple() + else: + self.bip = BIPForeign(bbmdAddress, bbmdTTL) self.annexj = AnnexJCodec() - self.mux = UDPMultiplexer(self.localAddress) + self.mux = UDPMultiplexer(self.localAddress, noBroadcast=True) # bind the bottom layers bind(self.bip, self.annexj, self.mux.annexJ) diff --git a/py27/bacpypes/appservice.py b/py27/bacpypes/appservice.py index 7adace61..c0d61d4a 100755 --- a/py27/bacpypes/appservice.py +++ b/py27/bacpypes/appservice.py @@ -50,7 +50,7 @@ class SSM(OneShotTask, DebugContents): _debug_contents = ('ssmSAP', 'localDevice', 'device_info', 'invokeID' , 'state', 'segmentAPDU', 'segmentSize', 'segmentCount', 'maxSegmentsAccepted' , 'retryCount', 'segmentRetryCount', 'sentAllSegments', 'lastSequenceNumber' - , 'initialSequenceNumber', 'actualWindowSize', 'proposedWindowSize' + , 'initialSequenceNumber', 'actualWindowSize' ) def __init__(self, sap, pdu_address): @@ -194,8 +194,8 @@ def get_segment(self, indx): # first segment sends proposed window size, rest get actual if indx == 0: - if _debug: SSM._debug(" - proposedWindowSize: %r", self.proposedWindowSize) - segAPDU.apduWin = self.proposedWindowSize + if _debug: SSM._debug(" - proposedWindowSize: %r", self.ssmSAP.proposedWindowSize) + segAPDU.apduWin = self.ssmSAP.proposedWindowSize else: if _debug: SSM._debug(" - actualWindowSize: %r", self.actualWindowSize) segAPDU.apduWin = self.actualWindowSize @@ -506,7 +506,7 @@ def segmented_request(self, apdu): self.set_segmentation_context(apdu) # minimum of what the server is proposing and this client proposes - self.actualWindowSize = min(apdu.apduWin, self.proposedWindowSize) + self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.proposedWindowSize) self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 self.set_state(SEGMENTED_CONFIRMATION, self.segmentTimeout) @@ -824,15 +824,15 @@ def confirmation(self, apdu): return # make sure client supports segmented receive - if self.device_info.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if not self.segmented_response_accepted: if _debug: ServerSSM._debug(" - client can't receive segmented responses") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return # 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: + # that the client said it was willing to accept in the request + if (self.maxSegmentsAccepted is not None) and (self.segmentCount > self.maxSegmentsAccepted): if _debug: ServerSSM._debug(" - client can't receive enough segments") abort = self.abort(AbortReason.apduTooLong) self.response(abort) @@ -897,11 +897,12 @@ def idle(self, apdu): self.invokeID = apdu.apduInvokeID if _debug: ServerSSM._debug(" - invoke ID: %r", self.invokeID) - if apdu.apduSA: - if not self.device_info: - if _debug: ServerSSM._debug(" - no client device info") + # remember if the client accepts segmented responses + self.segmented_response_accepted = apdu.apduSA - elif self.device_info.segmentationSupported == 'noSegmentation': + # if there is a cache record, check to see if it needs to be updated + if apdu.apduSA and self.device_info: + if self.device_info.segmentationSupported == 'noSegmentation': if _debug: ServerSSM._debug(" - client actually supports segmented receive") self.device_info.segmentationSupported = 'segmentedReceive' @@ -957,8 +958,11 @@ def idle(self, apdu): # 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) + self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.proposedWindowSize) + if _debug: ServerSSM._debug( + " - actualWindowSize? min(%r, %r) -> %r", + apdu.apduWin, self.ssmSAP.proposedWindowSize, self.actualWindowSize, + ) # initialize the state self.lastSequenceNumber = 0 diff --git a/py27/bacpypes/basetypes.py b/py27/bacpypes/basetypes.py index dcbd5ed8..6e5b75e5 100755 --- a/py27/bacpypes/basetypes.py +++ b/py27/bacpypes/basetypes.py @@ -8,7 +8,7 @@ from .primitivedata import BitString, Boolean, CharacterString, Date, Double, \ Enumerated, Integer, Null, ObjectIdentifier, OctetString, Real, Time, \ - Unsigned + Unsigned, Unsigned16 from .constructeddata import Any, AnyAtomic, ArrayOf, Choice, Element, \ Sequence, SequenceOf @@ -94,6 +94,7 @@ class ObjectTypesSupported(BitString): , 'accessUser':35 , 'accessZone':36 , 'credentialDataInput':37 + , 'networkPort':56 , 'networkSecurity':38 , 'bitstringValue':39 , 'characterstringValue':40 @@ -1101,9 +1102,50 @@ class ProgramState(Enumerated): } class PropertyIdentifier(Enumerated): + # TODO: Sort Alphabetically vendor_range = (512, 4194303) enumerations = \ { 'absenteeLimit':244 + , 'tags':486 + , 'profileLocation':91 + , 'eventDetectionEnabled':353 + , 'apduLength':388 + , 'linkSpeed':420 + , 'linkSpeeds':421 + , 'linkSpeedAutonegotiate':422 + , 'networkInterfaceName':424 + , 'bacnetIPMode':408 + , 'ipAddress':400 + , 'bacnetIPUDPPort':412 + , 'ipSubnetMask':411 + , 'ipDefaultGateway':401 + , 'bacnetIPMulticastAddress':409 + , 'ipDNSServer':406 + , 'ipDHCPEnable':402 + , 'ipDHCPLeaseTime':403 + , 'ipDHCPLeaseTimeRemaining':404 + , 'ipDHCPServer':405 + , 'bacnetIPNATTraversal':410 + , 'bacnetIPGlobalAddress':407 + , 'bbmdBroadcastDistributionTable':414 + , 'bbmdAcceptFDRegistrations':413 + , 'bbmdForeignDeviceTable':415 + , 'fdBBMDAddress':418 + , 'fdSubscriptionLifetime':419 + , 'bacnetIPv6Mode':435 + , 'ipv6Address':436 + , 'ipv6PrefixLength':437 + , 'bacnetIPv6UDPPort':438 + , 'ipv6DefaultGateway':439 + , 'bacnetIPv6MulticastAddress':440 + , 'ipv6DNSServer':441 + , 'ipv6AutoAddressingEnabled':442 + , 'ipv6DHCPLeaseTime':443 + , 'ipv6DHCPLeaseTimeRemaining':444 + , 'ipv6DHCPServer':445 + , 'ipv6ZoneIndex':446 + , 'virtualMACAddressTable':429 + , 'routingTable':428 , 'acceptedModes':175 , 'accessAlarmEvents':245 , 'accessDoors':246 @@ -1158,8 +1200,10 @@ class PropertyIdentifier(Enumerated): , 'bufferSize':126 , 'changeOfStateCount':15 , 'changeOfStateTime':16 + , 'changesPending':416 , 'channelNumber':366 , 'clientCovIncrement':127 + , 'command':417 , 'configurationFiles':154 , 'controlGroups':367 , 'controlledVariableReference':19 @@ -1286,6 +1330,7 @@ class PropertyIdentifier(Enumerated): , 'loggingRecord':184 , 'loggingType':197 , 'lowLimit':59 + , 'macAddress':423 , 'maintenanceRequired':158 , 'manipulatedVariableReference':60 , 'manualSlaveAddressBinding':170 @@ -1317,6 +1362,9 @@ class PropertyIdentifier(Enumerated): , 'musterPoint':287 , 'negativeAccessRules':288 , 'networkAccessSecurityPolicies':332 + , 'networkNumber':425 + , 'networkNumberQuality':427 + , 'networkType': 427 , 'nodeSubtype':207 , 'nodeType':208 , 'notificationClass':17 @@ -1365,6 +1413,7 @@ class PropertyIdentifier(Enumerated): , 'propertyList':371 , 'proportionalConstant':93 , 'proportionalConstantUnits':94 + , 'protocolLevel':482 , 'protocolObjectTypesSupported':96 , 'protocolRevision':139 , 'protocolServicesSupported':97 @@ -1376,6 +1425,7 @@ class PropertyIdentifier(Enumerated): , 'recipientList':102 , 'recordsSinceNotification':140 , 'recordCount':141 + , 'referencePort':483 , 'reliability':103 , 'reliabilityEvaluationInhibit':357 , 'relinquishDefault':104 @@ -1566,10 +1616,112 @@ class WriteStatus(Enumerated): , 'failed':3 } +class NetworkType(Enumerated): + enumerations = \ + { 'ethernet':0 + , 'arcnet':1 + , 'mstp':2 + , 'ptp':3 + , 'lontalk':4 + , 'ipv4':5 + , 'zigbee':6 + , 'virtual': 7 + # , 'non-bacnet': 8 Removed in Version 1, Revision 18 + , 'ipv6':9 + , 'serial':10 + } + +class ProtocolLevel(Enumerated): + enumerations = \ + { 'physical':0 + , 'protocol':1 + , 'bacnetApplication':2 + , 'nonBacnetApplication':3 + } + +class NetworkNumberQuality(Enumerated): + enumerations = \ + { 'unknown':0 + , 'learned':1 + , 'learnedConfigured':2 + , 'configured':3 + } + +class NetworkPortCommand(Enumerated): + enumerations = \ + { 'idle':0 + , 'discardChanges':1 + , 'renewFdDRegistration':2 + , 'restartSlaveDiscovery':3 + , 'renewDHCP':4 + , 'restartAutonegotiation':5 + , 'disconnect':6 + , 'restartPort':7 + } + +class IPMode(Enumerated): + enumerations = \ + { 'normal':0 + , 'foreign':1 + , 'bbmd':2 + } + +class RouterEntryStatus(Enumerated): + enumerations = \ + { 'available':0 + , 'busy':1 + , 'disconnected':2 + } + # # Forward Sequences # +class HostAddress(Choice): + choiceElements = \ + [ Element('none', Null) + , Element('ipAddress', OctetString) # 4 octets for B/IP or 16 octets for B/IPv6 + , Element('name', CharacterString) # Internet host name (see RFC 1123) + ] + +class HostNPort(Sequence): + sequenceElements = \ + [ Element('host', HostAddress) + , Element('port', Unsigned16) + ] + +class BDTEntry(Sequence): + sequenceElements = \ + [ Element('bbmdAddress', HostNPort) + , Element('broadcastMask', OctetString) # shall be present if BACnet/IP, and absent for BACnet/IPv6 + ] + +class FDTEntry(Sequence): + sequenceElements = \ + [ Element('bacnetIPAddress', OctetString) # the 6-octet B/IP or 18-octet B/IPv6 address of the registrant + , Element('timeToLive', Unsigned16) # time to live in seconds at the time of registration + , Element('remainingTimeToLive', Unsigned16) # remaining time to live in seconds, incl. grace period + ] + +class VMACEntry(Sequence): + sequenceElements = \ + [ Element('virtualMACAddress', OctetString) # maximum size 6 octets + , Element('nativeMACAddress', OctetString) + ] + +class RouterEntry(Sequence): + sequenceElements = \ + [ Element('networkNumber', Unsigned16) + , Element('macAddress', OctetString) + , Element('status', RouterEntryStatus) # Defined Above + ] + +class NameValue(Sequence): + sequenceElements = \ + [ Element('name', CharacterString) + , Element('value', AnyAtomic) # IS ATOMIC CORRECT HERE? value is limited to primitive datatypes and BACnetDateTime + ] + class DeviceAddress(Sequence): sequenceElements = \ [ Element('networkNumber', Unsigned) @@ -1730,7 +1882,8 @@ class AccessRule(Sequence): ] class AccessThreatLevel(Unsigned): - pass + _low_limit = 0 + _high_limit = 100 class AccumulatorRecord(Sequence): sequenceElements = \ diff --git a/py27/bacpypes/bvllservice.py b/py27/bacpypes/bvllservice.py index 8cc4ab88..558ac684 100755 --- a/py27/bacpypes/bvllservice.py +++ b/py27/bacpypes/bvllservice.py @@ -15,7 +15,7 @@ from .comm import Client, Server, bind, \ ServiceAccessPoint, ApplicationServiceElement -from .pdu import Address, LocalBroadcast, LocalStation, PDU, \ +from .pdu import Address, LocalBroadcast, PDU, \ unpack_ip_addr from .bvll import BVLPDU, DeleteForeignDeviceTableEntry, \ DistributeBroadcastToNetwork, FDTEntry, ForwardedNPDU, \ @@ -911,7 +911,7 @@ def register_foreign_device(self, addr, ttl): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation( addr ) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") @@ -936,7 +936,7 @@ def delete_foreign_device_table_entry(self, addr): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation( addr ) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") @@ -970,7 +970,7 @@ def add_peer(self, addr): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation(addr) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") @@ -988,7 +988,7 @@ def delete_peer(self, addr): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation(addr) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") @@ -1209,7 +1209,7 @@ def register_foreign_device(self, addr, ttl): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation( addr ) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") @@ -1234,7 +1234,7 @@ def delete_foreign_device_table_entry(self, addr): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation( addr ) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") @@ -1268,7 +1268,7 @@ def add_peer(self, addr): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation(addr) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") @@ -1290,7 +1290,7 @@ def delete_peer(self, addr): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation(addr) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") diff --git a/py27/bacpypes/constructeddata.py b/py27/bacpypes/constructeddata.py index 8dd27af4..c3431607 100755 --- a/py27/bacpypes/constructeddata.py +++ b/py27/bacpypes/constructeddata.py @@ -7,7 +7,7 @@ import sys from copy import deepcopy as _deepcopy -from .errors import DecodingError, \ +from .errors import DecodingError, EncodingError, \ MissingRequiredParameter, InvalidParameterDatatype, InvalidTag from .debugging import ModuleLogger, bacpypes_debugging @@ -1452,3 +1452,45 @@ def __repr__(self): return '<' + desc + ' instance at 0x%08x' % (id(self),) + '>' +# +# SequenceOfAny +# + +@bacpypes_debugging +class SequenceOfAny(Any): + + def cast_in(self, element): + """encode the element into the internal tag list.""" + if _debug: SequenceOfAny._debug("cast_in %r", element) + + # make sure it is a list + if not isinstance(element, List): + raise EncodingError("%r is not a list" % (element,)) + + t = TagList() + element.encode(t) + + self.tagList.extend(t.tagList) + + def cast_out(self, klass): + """Interpret the content as a particular class.""" + if _debug: SequenceOfAny._debug("cast_out %r", klass) + + # make sure it is a list + if not issubclass(klass, List): + raise DecodingError("%r is not a list" % (klass,)) + + # build a helper + helper = klass() + + # make a copy of the tag list + t = TagList(self.tagList[:]) + + # let it decode itself + helper.decode(t) + + # make sure everything was consumed + if len(t) != 0: + raise DecodingError("incomplete cast") + + return helper.value diff --git a/py27/bacpypes/debugging.py b/py27/bacpypes/debugging.py index 4adc9dd5..7a323bcf 100755 --- a/py27/bacpypes/debugging.py +++ b/py27/bacpypes/debugging.py @@ -10,20 +10,8 @@ import binascii from cStringIO import StringIO - -# set the level of the root logger -_root = logging.getLogger() -_root.setLevel(1) - -# add a stream handler for warnings and up -hdlr = logging.StreamHandler() -if ('--debugDebugging' in sys.argv): - hdlr.setLevel(logging.DEBUG) -else: - hdlr.setLevel(logging.WARNING) -hdlr.setFormatter(logging.Formatter(logging.BASIC_FORMAT, None)) -_root.addHandler(hdlr) -del hdlr +# create a root logger +root_logger = logging.getLogger('bacpypes') def btox(data, sep=''): @@ -68,12 +56,22 @@ def ModuleLogger(globs): if '_debug' not in globs: raise RuntimeError("define _debug before creating a module logger") + # logger name is the module name + logger_name = globs['__name__'] + # create a logger to be assigned to _log - logger = logging.getLogger(globs['__name__']) + logger = logging.getLogger(logger_name) # put in a reference to the module globals logger.globs = globs + # if this is a "root" logger add a default handler for warnings and up + if '.' not in logger_name: + hdlr = logging.StreamHandler() + hdlr.setLevel(logging.WARNING) + hdlr.setFormatter(logging.Formatter(logging.BASIC_FORMAT, None)) + logger.addHandler(hdlr) + return logger # diff --git a/py27/bacpypes/object.py b/py27/bacpypes/object.py index 8bbff2e0..2770068f 100755 --- a/py27/bacpypes/object.py +++ b/py27/bacpypes/object.py @@ -14,7 +14,7 @@ from .primitivedata import Atomic, BitString, Boolean, CharacterString, Date, \ Double, Integer, ObjectIdentifier, ObjectType, OctetString, Real, Time, \ - Unsigned + Unsigned, Unsigned8, Unsigned16 from .constructeddata import AnyAtomic, Array, ArrayOf, List, ListOf, \ Choice, Element, Sequence from .basetypes import AccessCredentialDisable, AccessCredentialDisableReason, \ @@ -22,24 +22,26 @@ AccessUserType, AccessZoneOccupancyState, AccumulatorRecord, Action, \ ActionList, AddressBinding, AssignedAccessRights, AuthenticationFactor, \ AuthenticationFactorFormat, AuthenticationPolicy, AuthenticationStatus, \ - AuthorizationException, AuthorizationMode, BackupState, BinaryPV, \ + AuthorizationException, AuthorizationMode, BackupState, BDTEntry, BinaryPV, \ COVSubscription, CalendarEntry, ChannelValue, ClientCOV, \ CredentialAuthenticationFactor, DailySchedule, DateRange, DateTime, \ Destination, DeviceObjectPropertyReference, DeviceObjectReference, \ DeviceStatus, DoorAlarmState, DoorSecuredStatus, DoorStatus, DoorValue, \ EngineeringUnits, EventNotificationSubscription, EventParameter, \ EventState, EventTransitionBits, EventType, FaultParameter, FaultType, \ - FileAccessMethod, LifeSafetyMode, LifeSafetyOperation, LifeSafetyState, \ + FileAccessMethod, FDTEntry, IPMode, HostNPort, LifeSafetyMode, LifeSafetyOperation, LifeSafetyState, \ LightingCommand, LightingInProgress, LightingTransition, LimitEnable, \ LockStatus, LogMultipleRecord, LogRecord, LogStatus, LoggingType, \ - Maintenance, NetworkSecurityPolicy, NodeType, NotifyType, \ + Maintenance, NameValue, NetworkNumberQuality, NetworkPortCommand, \ + NetworkSecurityPolicy, NetworkType, NodeType, NotifyType, \ ObjectPropertyReference, ObjectTypesSupported, OptionalCharacterString, \ Polarity, PortPermission, Prescale, PriorityArray, ProcessIdSelection, \ ProgramError, ProgramRequest, ProgramState, PropertyAccessResult, \ - PropertyIdentifier, Recipient, Reliability, RestartReason, Scale, \ - SecurityKeySet, SecurityLevel, Segmentation, ServicesSupported, \ - SetpointReference, ShedLevel, ShedState, SilencedState, SpecialEvent, \ - StatusFlags, TimeStamp, VTClass, VTSession, WriteStatus + PropertyIdentifier, ProtocolLevel, Recipient, Reliability, RestartReason, \ + RouterEntry, Scale, SecurityKeySet, SecurityLevel, Segmentation, \ + ServicesSupported, SetpointReference, ShedLevel, ShedState, SilencedState, \ + SpecialEvent, StatusFlags, TimeStamp, VTClass, VTSession, VMACEntry, \ + WriteStatus from .apdu import EventNotificationParameters, ReadAccessSpecification, \ ReadAccessResult @@ -1881,6 +1883,81 @@ class MultiStateValueObject(Object): , OptionalProperty('reliabilityEvaluationInhibit', Boolean) ] +@register_object_type +class NetworkPortObject(Object): + objectType = 'networkPort' #56 + properties = \ + [ ReadableProperty('statusFlags', StatusFlags) #111 + , ReadableProperty('reliability', Reliability) #103 + , ReadableProperty('outOfService', Boolean) #81 + , ReadableProperty('networkType', NetworkType) #427 + , ReadableProperty('protocolLevel', ProtocolLevel) #482 + , OptionalProperty('referencePort', Unsigned) #483 + , ReadableProperty('networkNumber', Unsigned16) #425 + , ReadableProperty('networkNumberQuality', NetworkNumberQuality) #427 + , ReadableProperty('changesPending', Boolean) #416 + , OptionalProperty('command', NetworkPortCommand) #417 + , OptionalProperty('macAddress', OctetString) #423 + , ReadableProperty('apduLength', Unsigned) #388 + , ReadableProperty('linkSpeed', Real) #420 + , OptionalProperty('linkSpeeds', ArrayOf(Real)) #421 + , OptionalProperty('eventTimeStamps', ArrayOf(TimeStamp)) #130 + , OptionalProperty('linkSpeedAutonegotiate', Boolean) #422 + , OptionalProperty('networkInterfaceName', CharacterString) #424 + , OptionalProperty('bacnetIPMode', IPMode) #408 + , OptionalProperty('ipAddress', OctetString) #400 + , OptionalProperty('bacnetIPUDPPort', Unsigned16) #412 + , OptionalProperty('ipSubnetMask', OctetString) #411 + , OptionalProperty('ipDefaultGateway', OctetString) #401 + , OptionalProperty('bacnetIPMulticastAddress', OctetString) #409 + , OptionalProperty('ipDNSServer', ArrayOf(OctetString)) #406 + , OptionalProperty('ipDHCPEnable', Boolean) #402 + , OptionalProperty('ipDHCPLeaseTime', Unsigned) #403 + , OptionalProperty('ipDHCPLeaseTimeRemaining', Unsigned) #404 + , OptionalProperty('ipDHCPServer', OctetString) #405 + , OptionalProperty('bacnetIPNATTraversal', Boolean) #410 + , OptionalProperty('bacnetIPGlobalAddress', HostNPort) #407 + , OptionalProperty('bbmdBroadcastDistributionTable', ListOf(BDTEntry)) #414 + , OptionalProperty('bbmdAcceptFDRegistrations', Boolean) #413 + , OptionalProperty('bbmdForeignDeviceTable', ListOf(FDTEntry)) #415 + , OptionalProperty('fdBBMDAddress', HostNPort) #418 + , OptionalProperty('fdSubscriptionLifetime', Unsigned16) #419 + , OptionalProperty('bacnetIPv6Mode', IPMode) #435 + , OptionalProperty('ipv6Address', OctetString) #436 + , OptionalProperty('ipv6PrefixLength', Unsigned8) #437 + , OptionalProperty('bacnetIPv6UDPPort', Unsigned16) #438 + , OptionalProperty('ipv6DefaultGateway', OctetString) #439 + , OptionalProperty('bacnetIPv6MulticastAddress', OctetString) #440 + , OptionalProperty('ipv6DNSServer', OctetString) #441 + , OptionalProperty('ipv6AutoAddressingEnabled', Boolean) #442 + , OptionalProperty('ipv6DHCPLeaseTime', Unsigned) #443 + , OptionalProperty('ipv6DHCPLeaseTimeRemaining', Unsigned) #444 + , OptionalProperty('ipv6DHCPServer', OctetString) #445 + , OptionalProperty('ipv6ZoneIndex', CharacterString) #446 + , OptionalProperty('maxMaster', Unsigned8) #64 + , OptionalProperty('maxInfoFrames', Unsigned8) #63 + , OptionalProperty('slaveProxyEnable', Boolean) #172 + , OptionalProperty('manualSlaveAddressBinding', ListOf(AddressBinding)) #170 + , OptionalProperty('autoSlaveDiscovery', Boolean) #169 + , OptionalProperty('slaveAddressBinding', ListOf(AddressBinding)) #171 + , OptionalProperty('virtualMACAddressTable', ListOf(VMACEntry)) #429 + , OptionalProperty('routingTable', ListOf(RouterEntry)) #428 + , OptionalProperty('eventDetectionEnabled', Boolean) #353 + , OptionalProperty('notificationClass', Unsigned) #17 + , OptionalProperty('eventEnable', EventTransitionBits) #35 + , OptionalProperty('ackedTransitions', EventTransitionBits) #0 + , OptionalProperty('notifyType', NotifyType) #72 + , OptionalProperty('eventTimeStamps', ArrayOf(TimeStamp, 3)) #130 + , OptionalProperty('eventMessageTexts', ArrayOf(CharacterString, 3)) #351 + , OptionalProperty('eventMessageTextsConfig', ArrayOf(CharacterString, 3)) #352 + , OptionalProperty('eventState', EventState) #36 + , ReadableProperty('reliabilityEvaluationInhibit', Boolean) #357 + , OptionalProperty('propertyList', ArrayOf(PropertyIdentifier)) #371 + , OptionalProperty('tags', ArrayOf(NameValue)) #486 + , OptionalProperty('profileLocation', CharacterString) #91 + , OptionalProperty('profileName', CharacterString) #168 + ] + @register_object_type class NetworkSecurityObject(Object): objectType = 'networkSecurity' diff --git a/py27/bacpypes/primitivedata.py b/py27/bacpypes/primitivedata.py index 8d00be62..43c09e9a 100755 --- a/py27/bacpypes/primitivedata.py +++ b/py27/bacpypes/primitivedata.py @@ -598,8 +598,10 @@ def __str__(self): class Unsigned(Atomic): _app_tag = Tag.unsignedAppTag + _low_limit = 0 + _high_limit = None - def __init__(self,arg = None): + def __init__(self, arg=None): self.value = 0L if arg is None: @@ -607,14 +609,16 @@ def __init__(self,arg = None): elif isinstance(arg, Tag): self.decode(arg) elif isinstance(arg, int): - if (arg < 0): - raise ValueError("unsigned integer required") + if not self.is_valid(arg): + raise ValueError("value out of range") self.value = long(arg) elif isinstance(arg, long): - if (arg < 0): - raise ValueError("unsigned integer required") + if not self.is_valid(arg): + raise ValueError("value out of range") self.value = arg elif isinstance(arg, Unsigned): + if not self.is_valid(arg.value): + raise ValueError("value out of range") self.value = arg.value else: raise TypeError("invalid constructor datatype") @@ -647,10 +651,24 @@ def decode(self, tag): @classmethod def is_valid(cls, arg): """Return True if arg is valid value for the class.""" - return isinstance(arg, (int, long)) and (not isinstance(arg, bool)) and (arg >= 0) + if not isinstance(arg, (int, long)) or isinstance(arg, bool): + return False + if (arg < cls._low_limit): + return False + if (cls._high_limit is not None) and (arg > cls._high_limit): + return False + return True def __str__(self): - return "Unsigned(%s)" % (self.value, ) + return "%s(%s)" % (self.__class__.__name__, self.value) + +class Unsigned8(Unsigned): + _low_limit = 0 + _high_limit = 255 + +class Unsigned16(Unsigned): + _low_limit = 0 + _high_limit = 65535 # # Integer diff --git a/py34/bacpypes/__init__.py b/py34/bacpypes/__init__.py index 10774264..18a2c1a0 100755 --- a/py34/bacpypes/__init__.py +++ b/py34/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.17.5' +__version__ = '0.17.6' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' diff --git a/py34/bacpypes/analysis.py b/py34/bacpypes/analysis.py index 2fe2aec4..df35fd9f 100755 --- a/py34/bacpypes/analysis.py +++ b/py34/bacpypes/analysis.py @@ -191,9 +191,13 @@ def decode_packet(data): if (pdu.pduData[0] == 0x81): if _debug: decode_packet._debug(" - BVLL header found") - xpdu = BVLPDU() - xpdu.decode(pdu) - pdu = xpdu + try: + xpdu = BVLPDU() + xpdu.decode(pdu) + pdu = xpdu + except Exception as err: + if _debug: decode_packet._debug(" - BVLPDU decoding error: %r", err) + return pdu # make a more focused interpretation atype = bvl_pdu_types.get(pdu.bvlciFunction) @@ -360,8 +364,12 @@ def decode_file(fname): # loop through the packets for i, (timestamp, data) in enumerate(p): - pkt = decode_packet(data) - if not pkt: + try: + pkt = decode_packet(data) + if not pkt: + continue + except Exception as err: + if _debug: decode_file._debug(" - exception decoding packet %d: %r", i+1, err) continue # save the packet number (as viewed in Wireshark) and timestamp diff --git a/py34/bacpypes/apdu.py b/py34/bacpypes/apdu.py index c19a811e..acf2c1a2 100755 --- a/py34/bacpypes/apdu.py +++ b/py34/bacpypes/apdu.py @@ -920,7 +920,7 @@ class RangeByPosition(Sequence): class RangeBySequenceNumber(Sequence): sequenceElements = \ - [ Element('referenceIndex', Unsigned) + [ Element('referenceSequenceNumber', Unsigned) , Element('count', Integer) ] diff --git a/py34/bacpypes/app.py b/py34/bacpypes/app.py index 1548a3b4..b7b54f56 100755 --- a/py34/bacpypes/app.py +++ b/py34/bacpypes/app.py @@ -593,7 +593,7 @@ def close_socket(self): @bacpypes_debugging class BIPNetworkApplication(NetworkServiceElement): - def __init__(self, localAddress, eID=None): + def __init__(self, localAddress, bbmdAddress=None, bbmdTTL=None, eID=None): if _debug: BIPNetworkApplication._debug("__init__ %r eID=%r", localAddress, eID) NetworkServiceElement.__init__(self, eID) @@ -611,9 +611,12 @@ def __init__(self, localAddress, eID=None): # create a generic BIP stack, bound to the Annex J server # on the UDP multiplexer - self.bip = BIPSimple() + if (not bbmdAddress) and (not bbmdTTL): + self.bip = BIPSimple() + else: + self.bip = BIPForeign(bbmdAddress, bbmdTTL) self.annexj = AnnexJCodec() - self.mux = UDPMultiplexer(self.localAddress) + self.mux = UDPMultiplexer(self.localAddress, noBroadcast=True) # bind the bottom layers bind(self.bip, self.annexj, self.mux.annexJ) diff --git a/py34/bacpypes/appservice.py b/py34/bacpypes/appservice.py index 75591f79..e4390b1f 100755 --- a/py34/bacpypes/appservice.py +++ b/py34/bacpypes/appservice.py @@ -50,7 +50,7 @@ class SSM(OneShotTask, DebugContents): _debug_contents = ('ssmSAP', 'localDevice', 'device_info', 'invokeID' , 'state', 'segmentAPDU', 'segmentSize', 'segmentCount', 'maxSegmentsAccepted' , 'retryCount', 'segmentRetryCount', 'sentAllSegments', 'lastSequenceNumber' - , 'initialSequenceNumber', 'actualWindowSize', 'proposedWindowSize' + , 'initialSequenceNumber', 'actualWindowSize' ) def __init__(self, sap, pdu_address): @@ -194,8 +194,8 @@ def get_segment(self, indx): # first segment sends proposed window size, rest get actual if indx == 0: - if _debug: SSM._debug(" - proposedWindowSize: %r", self.proposedWindowSize) - segAPDU.apduWin = self.proposedWindowSize + if _debug: SSM._debug(" - proposedWindowSize: %r", self.ssmSAP.proposedWindowSize) + segAPDU.apduWin = self.ssmSAP.proposedWindowSize else: if _debug: SSM._debug(" - actualWindowSize: %r", self.actualWindowSize) segAPDU.apduWin = self.actualWindowSize @@ -506,7 +506,7 @@ def segmented_request(self, apdu): self.set_segmentation_context(apdu) # minimum of what the server is proposing and this client proposes - self.actualWindowSize = min(apdu.apduWin, self.proposedWindowSize) + self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.proposedWindowSize) self.lastSequenceNumber = 0 self.initialSequenceNumber = 0 self.set_state(SEGMENTED_CONFIRMATION, self.segmentTimeout) @@ -824,15 +824,15 @@ def confirmation(self, apdu): return # make sure client supports segmented receive - if self.device_info.segmentationSupported not in ('segmentedReceive', 'segmentedBoth'): + if not self.segmented_response_accepted: if _debug: ServerSSM._debug(" - client can't receive segmented responses") abort = self.abort(AbortReason.segmentationNotSupported) self.response(abort) return # 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: + # that the client said it was willing to accept in the request + if (self.maxSegmentsAccepted is not None) and (self.segmentCount > self.maxSegmentsAccepted): if _debug: ServerSSM._debug(" - client can't receive enough segments") abort = self.abort(AbortReason.apduTooLong) self.response(abort) @@ -897,11 +897,12 @@ def idle(self, apdu): self.invokeID = apdu.apduInvokeID if _debug: ServerSSM._debug(" - invoke ID: %r", self.invokeID) - if apdu.apduSA: - if not self.device_info: - if _debug: ServerSSM._debug(" - no client device info") + # remember if the client accepts segmented responses + self.segmented_response_accepted = apdu.apduSA - elif self.device_info.segmentationSupported == 'noSegmentation': + # if there is a cache record, check to see if it needs to be updated + if apdu.apduSA and self.device_info: + if self.device_info.segmentationSupported == 'noSegmentation': if _debug: ServerSSM._debug(" - client actually supports segmented receive") self.device_info.segmentationSupported = 'segmentedReceive' @@ -957,8 +958,11 @@ def idle(self, apdu): # 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) + self.actualWindowSize = min(apdu.apduWin, self.ssmSAP.proposedWindowSize) + if _debug: ServerSSM._debug( + " - actualWindowSize? min(%r, %r) -> %r", + apdu.apduWin, self.ssmSAP.proposedWindowSize, self.actualWindowSize, + ) # initialize the state self.lastSequenceNumber = 0 diff --git a/py34/bacpypes/basetypes.py b/py34/bacpypes/basetypes.py index dcbd5ed8..6e5b75e5 100755 --- a/py34/bacpypes/basetypes.py +++ b/py34/bacpypes/basetypes.py @@ -8,7 +8,7 @@ from .primitivedata import BitString, Boolean, CharacterString, Date, Double, \ Enumerated, Integer, Null, ObjectIdentifier, OctetString, Real, Time, \ - Unsigned + Unsigned, Unsigned16 from .constructeddata import Any, AnyAtomic, ArrayOf, Choice, Element, \ Sequence, SequenceOf @@ -94,6 +94,7 @@ class ObjectTypesSupported(BitString): , 'accessUser':35 , 'accessZone':36 , 'credentialDataInput':37 + , 'networkPort':56 , 'networkSecurity':38 , 'bitstringValue':39 , 'characterstringValue':40 @@ -1101,9 +1102,50 @@ class ProgramState(Enumerated): } class PropertyIdentifier(Enumerated): + # TODO: Sort Alphabetically vendor_range = (512, 4194303) enumerations = \ { 'absenteeLimit':244 + , 'tags':486 + , 'profileLocation':91 + , 'eventDetectionEnabled':353 + , 'apduLength':388 + , 'linkSpeed':420 + , 'linkSpeeds':421 + , 'linkSpeedAutonegotiate':422 + , 'networkInterfaceName':424 + , 'bacnetIPMode':408 + , 'ipAddress':400 + , 'bacnetIPUDPPort':412 + , 'ipSubnetMask':411 + , 'ipDefaultGateway':401 + , 'bacnetIPMulticastAddress':409 + , 'ipDNSServer':406 + , 'ipDHCPEnable':402 + , 'ipDHCPLeaseTime':403 + , 'ipDHCPLeaseTimeRemaining':404 + , 'ipDHCPServer':405 + , 'bacnetIPNATTraversal':410 + , 'bacnetIPGlobalAddress':407 + , 'bbmdBroadcastDistributionTable':414 + , 'bbmdAcceptFDRegistrations':413 + , 'bbmdForeignDeviceTable':415 + , 'fdBBMDAddress':418 + , 'fdSubscriptionLifetime':419 + , 'bacnetIPv6Mode':435 + , 'ipv6Address':436 + , 'ipv6PrefixLength':437 + , 'bacnetIPv6UDPPort':438 + , 'ipv6DefaultGateway':439 + , 'bacnetIPv6MulticastAddress':440 + , 'ipv6DNSServer':441 + , 'ipv6AutoAddressingEnabled':442 + , 'ipv6DHCPLeaseTime':443 + , 'ipv6DHCPLeaseTimeRemaining':444 + , 'ipv6DHCPServer':445 + , 'ipv6ZoneIndex':446 + , 'virtualMACAddressTable':429 + , 'routingTable':428 , 'acceptedModes':175 , 'accessAlarmEvents':245 , 'accessDoors':246 @@ -1158,8 +1200,10 @@ class PropertyIdentifier(Enumerated): , 'bufferSize':126 , 'changeOfStateCount':15 , 'changeOfStateTime':16 + , 'changesPending':416 , 'channelNumber':366 , 'clientCovIncrement':127 + , 'command':417 , 'configurationFiles':154 , 'controlGroups':367 , 'controlledVariableReference':19 @@ -1286,6 +1330,7 @@ class PropertyIdentifier(Enumerated): , 'loggingRecord':184 , 'loggingType':197 , 'lowLimit':59 + , 'macAddress':423 , 'maintenanceRequired':158 , 'manipulatedVariableReference':60 , 'manualSlaveAddressBinding':170 @@ -1317,6 +1362,9 @@ class PropertyIdentifier(Enumerated): , 'musterPoint':287 , 'negativeAccessRules':288 , 'networkAccessSecurityPolicies':332 + , 'networkNumber':425 + , 'networkNumberQuality':427 + , 'networkType': 427 , 'nodeSubtype':207 , 'nodeType':208 , 'notificationClass':17 @@ -1365,6 +1413,7 @@ class PropertyIdentifier(Enumerated): , 'propertyList':371 , 'proportionalConstant':93 , 'proportionalConstantUnits':94 + , 'protocolLevel':482 , 'protocolObjectTypesSupported':96 , 'protocolRevision':139 , 'protocolServicesSupported':97 @@ -1376,6 +1425,7 @@ class PropertyIdentifier(Enumerated): , 'recipientList':102 , 'recordsSinceNotification':140 , 'recordCount':141 + , 'referencePort':483 , 'reliability':103 , 'reliabilityEvaluationInhibit':357 , 'relinquishDefault':104 @@ -1566,10 +1616,112 @@ class WriteStatus(Enumerated): , 'failed':3 } +class NetworkType(Enumerated): + enumerations = \ + { 'ethernet':0 + , 'arcnet':1 + , 'mstp':2 + , 'ptp':3 + , 'lontalk':4 + , 'ipv4':5 + , 'zigbee':6 + , 'virtual': 7 + # , 'non-bacnet': 8 Removed in Version 1, Revision 18 + , 'ipv6':9 + , 'serial':10 + } + +class ProtocolLevel(Enumerated): + enumerations = \ + { 'physical':0 + , 'protocol':1 + , 'bacnetApplication':2 + , 'nonBacnetApplication':3 + } + +class NetworkNumberQuality(Enumerated): + enumerations = \ + { 'unknown':0 + , 'learned':1 + , 'learnedConfigured':2 + , 'configured':3 + } + +class NetworkPortCommand(Enumerated): + enumerations = \ + { 'idle':0 + , 'discardChanges':1 + , 'renewFdDRegistration':2 + , 'restartSlaveDiscovery':3 + , 'renewDHCP':4 + , 'restartAutonegotiation':5 + , 'disconnect':6 + , 'restartPort':7 + } + +class IPMode(Enumerated): + enumerations = \ + { 'normal':0 + , 'foreign':1 + , 'bbmd':2 + } + +class RouterEntryStatus(Enumerated): + enumerations = \ + { 'available':0 + , 'busy':1 + , 'disconnected':2 + } + # # Forward Sequences # +class HostAddress(Choice): + choiceElements = \ + [ Element('none', Null) + , Element('ipAddress', OctetString) # 4 octets for B/IP or 16 octets for B/IPv6 + , Element('name', CharacterString) # Internet host name (see RFC 1123) + ] + +class HostNPort(Sequence): + sequenceElements = \ + [ Element('host', HostAddress) + , Element('port', Unsigned16) + ] + +class BDTEntry(Sequence): + sequenceElements = \ + [ Element('bbmdAddress', HostNPort) + , Element('broadcastMask', OctetString) # shall be present if BACnet/IP, and absent for BACnet/IPv6 + ] + +class FDTEntry(Sequence): + sequenceElements = \ + [ Element('bacnetIPAddress', OctetString) # the 6-octet B/IP or 18-octet B/IPv6 address of the registrant + , Element('timeToLive', Unsigned16) # time to live in seconds at the time of registration + , Element('remainingTimeToLive', Unsigned16) # remaining time to live in seconds, incl. grace period + ] + +class VMACEntry(Sequence): + sequenceElements = \ + [ Element('virtualMACAddress', OctetString) # maximum size 6 octets + , Element('nativeMACAddress', OctetString) + ] + +class RouterEntry(Sequence): + sequenceElements = \ + [ Element('networkNumber', Unsigned16) + , Element('macAddress', OctetString) + , Element('status', RouterEntryStatus) # Defined Above + ] + +class NameValue(Sequence): + sequenceElements = \ + [ Element('name', CharacterString) + , Element('value', AnyAtomic) # IS ATOMIC CORRECT HERE? value is limited to primitive datatypes and BACnetDateTime + ] + class DeviceAddress(Sequence): sequenceElements = \ [ Element('networkNumber', Unsigned) @@ -1730,7 +1882,8 @@ class AccessRule(Sequence): ] class AccessThreatLevel(Unsigned): - pass + _low_limit = 0 + _high_limit = 100 class AccumulatorRecord(Sequence): sequenceElements = \ diff --git a/py34/bacpypes/bvllservice.py b/py34/bacpypes/bvllservice.py index a2a2248b..42a08569 100755 --- a/py34/bacpypes/bvllservice.py +++ b/py34/bacpypes/bvllservice.py @@ -15,7 +15,7 @@ from .comm import Client, Server, bind, \ ServiceAccessPoint, ApplicationServiceElement -from .pdu import Address, LocalBroadcast, LocalStation, PDU, \ +from .pdu import Address, LocalBroadcast, PDU, \ unpack_ip_addr from .bvll import BVLPDU, DeleteForeignDeviceTableEntry, \ DistributeBroadcastToNetwork, FDTEntry, ForwardedNPDU, \ @@ -910,7 +910,7 @@ def register_foreign_device(self, addr, ttl): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation( addr ) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") @@ -935,7 +935,7 @@ def delete_foreign_device_table_entry(self, addr): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation( addr ) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") @@ -969,7 +969,7 @@ def add_peer(self, addr): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation(addr) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") @@ -987,7 +987,7 @@ def delete_peer(self, addr): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation(addr) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") @@ -1208,7 +1208,7 @@ def register_foreign_device(self, addr, ttl): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation( addr ) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") @@ -1233,7 +1233,7 @@ def delete_foreign_device_table_entry(self, addr): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation( addr ) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") @@ -1267,7 +1267,7 @@ def add_peer(self, addr): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation(addr) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") @@ -1289,7 +1289,7 @@ def delete_peer(self, addr): if isinstance(addr, Address): pass elif isinstance(addr, str): - addr = LocalStation(addr) + addr = Address(addr) else: raise TypeError("addr must be a string or an Address") diff --git a/py34/bacpypes/constructeddata.py b/py34/bacpypes/constructeddata.py index 8dd27af4..c3431607 100755 --- a/py34/bacpypes/constructeddata.py +++ b/py34/bacpypes/constructeddata.py @@ -7,7 +7,7 @@ import sys from copy import deepcopy as _deepcopy -from .errors import DecodingError, \ +from .errors import DecodingError, EncodingError, \ MissingRequiredParameter, InvalidParameterDatatype, InvalidTag from .debugging import ModuleLogger, bacpypes_debugging @@ -1452,3 +1452,45 @@ def __repr__(self): return '<' + desc + ' instance at 0x%08x' % (id(self),) + '>' +# +# SequenceOfAny +# + +@bacpypes_debugging +class SequenceOfAny(Any): + + def cast_in(self, element): + """encode the element into the internal tag list.""" + if _debug: SequenceOfAny._debug("cast_in %r", element) + + # make sure it is a list + if not isinstance(element, List): + raise EncodingError("%r is not a list" % (element,)) + + t = TagList() + element.encode(t) + + self.tagList.extend(t.tagList) + + def cast_out(self, klass): + """Interpret the content as a particular class.""" + if _debug: SequenceOfAny._debug("cast_out %r", klass) + + # make sure it is a list + if not issubclass(klass, List): + raise DecodingError("%r is not a list" % (klass,)) + + # build a helper + helper = klass() + + # make a copy of the tag list + t = TagList(self.tagList[:]) + + # let it decode itself + helper.decode(t) + + # make sure everything was consumed + if len(t) != 0: + raise DecodingError("incomplete cast") + + return helper.value diff --git a/py34/bacpypes/debugging.py b/py34/bacpypes/debugging.py index b6eb804b..b0abb024 100755 --- a/py34/bacpypes/debugging.py +++ b/py34/bacpypes/debugging.py @@ -10,20 +10,8 @@ import binascii from io import StringIO - -# set the level of the root logger -_root = logging.getLogger() -_root.setLevel(1) - -# add a stream handler for warnings and up -hdlr = logging.StreamHandler() -if ('--debugDebugging' in sys.argv): - hdlr.setLevel(logging.DEBUG) -else: - hdlr.setLevel(logging.WARNING) -hdlr.setFormatter(logging.Formatter(logging.BASIC_FORMAT, None)) -_root.addHandler(hdlr) -del hdlr +# create a root logger +root_logger = logging.getLogger('bacpypes') def btox(data, sep=''): @@ -68,12 +56,22 @@ def ModuleLogger(globs): if '_debug' not in globs: raise RuntimeError("define _debug before creating a module logger") + # logger name is the module name + logger_name = globs['__name__'] + # create a logger to be assigned to _log - logger = logging.getLogger(globs['__name__']) + logger = logging.getLogger(logger_name) # put in a reference to the module globals logger.globs = globs + # if this is a "root" logger add a default handler for warnings and up + if '.' not in logger_name: + hdlr = logging.StreamHandler() + hdlr.setLevel(logging.WARNING) + hdlr.setFormatter(logging.Formatter(logging.BASIC_FORMAT, None)) + logger.addHandler(hdlr) + return logger # diff --git a/py34/bacpypes/object.py b/py34/bacpypes/object.py old mode 100755 new mode 100644 index 8308abe8..223d5ba2 --- a/py34/bacpypes/object.py +++ b/py34/bacpypes/object.py @@ -14,7 +14,7 @@ from .primitivedata import Atomic, BitString, Boolean, CharacterString, Date, \ Double, Integer, ObjectIdentifier, ObjectType, OctetString, Real, Time, \ - Unsigned + Unsigned, Unsigned8, Unsigned16 from .constructeddata import AnyAtomic, Array, ArrayOf, List, ListOf, \ Choice, Element, Sequence from .basetypes import AccessCredentialDisable, AccessCredentialDisableReason, \ @@ -22,24 +22,26 @@ AccessUserType, AccessZoneOccupancyState, AccumulatorRecord, Action, \ ActionList, AddressBinding, AssignedAccessRights, AuthenticationFactor, \ AuthenticationFactorFormat, AuthenticationPolicy, AuthenticationStatus, \ - AuthorizationException, AuthorizationMode, BackupState, BinaryPV, \ + AuthorizationException, AuthorizationMode, BackupState, BDTEntry, BinaryPV, \ COVSubscription, CalendarEntry, ChannelValue, ClientCOV, \ CredentialAuthenticationFactor, DailySchedule, DateRange, DateTime, \ Destination, DeviceObjectPropertyReference, DeviceObjectReference, \ DeviceStatus, DoorAlarmState, DoorSecuredStatus, DoorStatus, DoorValue, \ EngineeringUnits, EventNotificationSubscription, EventParameter, \ EventState, EventTransitionBits, EventType, FaultParameter, FaultType, \ - FileAccessMethod, LifeSafetyMode, LifeSafetyOperation, LifeSafetyState, \ + FileAccessMethod, FDTEntry, IPMode, HostNPort, LifeSafetyMode, LifeSafetyOperation, LifeSafetyState, \ LightingCommand, LightingInProgress, LightingTransition, LimitEnable, \ LockStatus, LogMultipleRecord, LogRecord, LogStatus, LoggingType, \ - Maintenance, NetworkSecurityPolicy, NodeType, NotifyType, \ + Maintenance, NameValue, NetworkNumberQuality, NetworkPortCommand, \ + NetworkSecurityPolicy, NetworkType, NodeType, NotifyType, \ ObjectPropertyReference, ObjectTypesSupported, OptionalCharacterString, \ Polarity, PortPermission, Prescale, PriorityArray, ProcessIdSelection, \ ProgramError, ProgramRequest, ProgramState, PropertyAccessResult, \ - PropertyIdentifier, Recipient, Reliability, RestartReason, Scale, \ - SecurityKeySet, SecurityLevel, Segmentation, ServicesSupported, \ - SetpointReference, ShedLevel, ShedState, SilencedState, SpecialEvent, \ - StatusFlags, TimeStamp, VTClass, VTSession, WriteStatus + PropertyIdentifier, ProtocolLevel, Recipient, Reliability, RestartReason, \ + RouterEntry, Scale, SecurityKeySet, SecurityLevel, Segmentation, \ + ServicesSupported, SetpointReference, ShedLevel, ShedState, SilencedState, \ + SpecialEvent, StatusFlags, TimeStamp, VTClass, VTSession, VMACEntry, \ + WriteStatus from .apdu import EventNotificationParameters, ReadAccessSpecification, \ ReadAccessResult @@ -329,6 +331,8 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False arry[arrayIndex] = value except IndexError: raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') + except TypeError: + raise ExecutionError(errorClass='property', errorCode='valueOutOfRange') # check for monitors, call each one with the old and new value if is_monitored: @@ -1879,6 +1883,81 @@ class MultiStateValueObject(Object): , OptionalProperty('reliabilityEvaluationInhibit', Boolean) ] +@register_object_type +class NetworkPortObject(Object): + objectType = 'networkPort' #56 + properties = \ + [ ReadableProperty('statusFlags', StatusFlags) #111 + , ReadableProperty('reliability', Reliability) #103 + , ReadableProperty('outOfService', Boolean) #81 + , ReadableProperty('networkType', NetworkType) #427 + , ReadableProperty('protocolLevel', ProtocolLevel) #482 + , OptionalProperty('referencePort', Unsigned) #483 + , ReadableProperty('networkNumber', Unsigned16) #425 + , ReadableProperty('networkNumberQuality', NetworkNumberQuality) #427 + , ReadableProperty('changesPending', Boolean) #416 + , OptionalProperty('command', NetworkPortCommand) #417 + , OptionalProperty('macAddress', OctetString) #423 + , ReadableProperty('apduLength', Unsigned) #388 + , ReadableProperty('linkSpeed', Real) #420 + , OptionalProperty('linkSpeeds', ArrayOf(Real)) #421 + , OptionalProperty('eventTimeStamps', ArrayOf(TimeStamp)) #130 + , OptionalProperty('linkSpeedAutonegotiate', Boolean) #422 + , OptionalProperty('networkInterfaceName', CharacterString) #424 + , OptionalProperty('bacnetIPMode', IPMode) #408 + , OptionalProperty('ipAddress', OctetString) #400 + , OptionalProperty('bacnetIPUDPPort', Unsigned16) #412 + , OptionalProperty('ipSubnetMask', OctetString) #411 + , OptionalProperty('ipDefaultGateway', OctetString) #401 + , OptionalProperty('bacnetIPMulticastAddress', OctetString) #409 + , OptionalProperty('ipDNSServer', ArrayOf(OctetString)) #406 + , OptionalProperty('ipDHCPEnable', Boolean) #402 + , OptionalProperty('ipDHCPLeaseTime', Unsigned) #403 + , OptionalProperty('ipDHCPLeaseTimeRemaining', Unsigned) #404 + , OptionalProperty('ipDHCPServer', OctetString) #405 + , OptionalProperty('bacnetIPNATTraversal', Boolean) #410 + , OptionalProperty('bacnetIPGlobalAddress', HostNPort) #407 + , OptionalProperty('bbmdBroadcastDistributionTable', ListOf(BDTEntry)) #414 + , OptionalProperty('bbmdAcceptFDRegistrations', Boolean) #413 + , OptionalProperty('bbmdForeignDeviceTable', ListOf(FDTEntry)) #415 + , OptionalProperty('fdBBMDAddress', HostNPort) #418 + , OptionalProperty('fdSubscriptionLifetime', Unsigned16) #419 + , OptionalProperty('bacnetIPv6Mode', IPMode) #435 + , OptionalProperty('ipv6Address', OctetString) #436 + , OptionalProperty('ipv6PrefixLength', Unsigned8) #437 + , OptionalProperty('bacnetIPv6UDPPort', Unsigned16) #438 + , OptionalProperty('ipv6DefaultGateway', OctetString) #439 + , OptionalProperty('bacnetIPv6MulticastAddress', OctetString) #440 + , OptionalProperty('ipv6DNSServer', OctetString) #441 + , OptionalProperty('ipv6AutoAddressingEnabled', Boolean) #442 + , OptionalProperty('ipv6DHCPLeaseTime', Unsigned) #443 + , OptionalProperty('ipv6DHCPLeaseTimeRemaining', Unsigned) #444 + , OptionalProperty('ipv6DHCPServer', OctetString) #445 + , OptionalProperty('ipv6ZoneIndex', CharacterString) #446 + , OptionalProperty('maxMaster', Unsigned8) #64 + , OptionalProperty('maxInfoFrames', Unsigned8) #63 + , OptionalProperty('slaveProxyEnable', Boolean) #172 + , OptionalProperty('manualSlaveAddressBinding', ListOf(AddressBinding)) #170 + , OptionalProperty('autoSlaveDiscovery', Boolean) #169 + , OptionalProperty('slaveAddressBinding', ListOf(AddressBinding)) #171 + , OptionalProperty('virtualMACAddressTable', ListOf(VMACEntry)) #429 + , OptionalProperty('routingTable', ListOf(RouterEntry)) #428 + , OptionalProperty('eventDetectionEnabled', Boolean) #353 + , OptionalProperty('notificationClass', Unsigned) #17 + , OptionalProperty('eventEnable', EventTransitionBits) #35 + , OptionalProperty('ackedTransitions', EventTransitionBits) #0 + , OptionalProperty('notifyType', NotifyType) #72 + , OptionalProperty('eventTimeStamps', ArrayOf(TimeStamp, 3)) #130 + , OptionalProperty('eventMessageTexts', ArrayOf(CharacterString, 3)) #351 + , OptionalProperty('eventMessageTextsConfig', ArrayOf(CharacterString, 3)) #352 + , OptionalProperty('eventState', EventState) #36 + , ReadableProperty('reliabilityEvaluationInhibit', Boolean) #357 + , OptionalProperty('propertyList', ArrayOf(PropertyIdentifier)) #371 + , OptionalProperty('tags', ArrayOf(NameValue)) #486 + , OptionalProperty('profileLocation', CharacterString) #91 + , OptionalProperty('profileName', CharacterString) #168 + ] + @register_object_type class NetworkSecurityObject(Object): objectType = 'networkSecurity' diff --git a/py34/bacpypes/primitivedata.py b/py34/bacpypes/primitivedata.py index bb46f082..d3b341ca 100755 --- a/py34/bacpypes/primitivedata.py +++ b/py34/bacpypes/primitivedata.py @@ -598,8 +598,10 @@ def __str__(self): class Unsigned(Atomic): _app_tag = Tag.unsignedAppTag + _low_limit = 0 + _high_limit = None - def __init__(self,arg = None): + def __init__(self, arg=None): self.value = 0 if arg is None: @@ -607,10 +609,12 @@ def __init__(self,arg = None): elif isinstance(arg, Tag): self.decode(arg) elif isinstance(arg, int): - if (arg < 0): - raise ValueError("unsigned integer required") + if not self.is_valid(arg): + raise ValueError("value out of range") self.value = arg elif isinstance(arg, Unsigned): + if not self.is_valid(arg.value): + raise ValueError("value out of range") self.value = arg.value else: raise TypeError("invalid constructor datatype") @@ -643,10 +647,24 @@ def decode(self, tag): @classmethod def is_valid(cls, arg): """Return True if arg is valid value for the class.""" - return isinstance(arg, int) and (not isinstance(arg, bool)) and (arg >= 0) + if not isinstance(arg, int) or isinstance(arg, bool): + return False + if (arg < cls._low_limit): + return False + if (cls._high_limit is not None) and (arg > cls._high_limit): + return False + return True def __str__(self): - return "Unsigned(%s)" % (self.value, ) + return "%s(%s)" % (self.__class__.__name__, self.value) + +class Unsigned8(Unsigned): + _low_limit = 0 + _high_limit = 255 + +class Unsigned16(Unsigned): + _low_limit = 0 + _high_limit = 65535 # # Integer diff --git a/samples/BBMD2VLANRouter.py b/samples/BBMD2VLANRouter.py index d85b3a42..9e274e89 100755 --- a/samples/BBMD2VLANRouter.py +++ b/samples/BBMD2VLANRouter.py @@ -90,7 +90,7 @@ class VLANApplication(Application, WhoIsIAmServices, ReadWritePropertyServices): def __init__(self, vlan_device, vlan_address, aseID=None): if _debug: VLANApplication._debug("__init__ %r %r aseID=%r", vlan_device, vlan_address, aseID) - Application.__init__(self, vlan_device, vlan_address, aseID) + Application.__init__(self, vlan_device, aseID=aseID) # include a application decoder self.asap = ApplicationServiceAccessPoint() diff --git a/samples/COVClient.py b/samples/COVClient.py index e7816edd..be02d1f4 100755 --- a/samples/COVClient.py +++ b/samples/COVClient.py @@ -11,7 +11,7 @@ from bacpypes.consolelogging import ConfigArgumentParser from bacpypes.consolecmd import ConsoleCmd -from bacpypes.core import run, enable_sleeping +from bacpypes.core import run, deferred, enable_sleeping from bacpypes.iocb import IOCB from bacpypes.pdu import Address @@ -145,7 +145,7 @@ def do_subscribe(self, args): if _debug: SubscribeCOVConsoleCmd._debug(" - iocb: %r", iocb) # give it to the application - this_application.request_io(iocb) + deferred(this_application.request_io, iocb) # wait for it to complete iocb.wait() diff --git a/samples/DeviceCommunicationControl.py b/samples/DeviceCommunicationControl.py index 3f5b4a11..dd5b6061 100755 --- a/samples/DeviceCommunicationControl.py +++ b/samples/DeviceCommunicationControl.py @@ -11,7 +11,7 @@ from bacpypes.consolelogging import ConfigArgumentParser from bacpypes.consolecmd import ConsoleCmd -from bacpypes.core import run, enable_sleeping +from bacpypes.core import run, deferred, enable_sleeping from bacpypes.iocb import IOCB from bacpypes.pdu import Address @@ -78,7 +78,7 @@ def do_dcc(self, args): if _debug: DCCConsoleCmd._debug(" - iocb: %r", iocb) # give it to the application - this_application.request_io(iocb) + deferred(this_application.request_io, iocb) # wait for it to complete iocb.wait() diff --git a/samples/Discover.py b/samples/Discover.py new file mode 100644 index 00000000..af9d501b --- /dev/null +++ b/samples/Discover.py @@ -0,0 +1,866 @@ +#!/usr/bin/python3 + +""" +Discover + +This is a console application to assist with the "discovery" process of finding +BACnet routers, devices, objects, and property values. It reads and/or writes +a tab-delimited property values text file of the values that it has received. + +The console commands are: + + wirtn - who is router to network + irt - initialize routing table + winn - what is network number + whois - who is + iam - i am + rp - read property + rpm - read property multiple + +The property values text file contains these fields: + + devid - device identifier + objid - object identifier + propid - property identifier + version - version number + value - value + +When the object identifier field is "-" the properties are from I-Am messages +that are received: + + address - the BACpypes address of the device + maxAPDULengthAccepted - maximum APDU length accepted + segmentationSupported - segmentation supported + +To facilitate finding out what has changed between two different times the +application has been run the property values text file is always sorted. +Whenever a property value is different than the previous value, the version +number is incremented. + +For example, the first time the presentValue of analogValue:1 is read the +file could look like this: + + 202 analogValue:1 presentValue 1 14.02 + +Then when the application is run again, it could look like this: + + 202 analogValue:1 presentValue 2 9.52 + +The application accepts stdin from non-interactive sessions, for example: + + $ echo "whois" | python Discover.py stuff + +or from a script file: + + $ python discover.py stuff << EOF + > whois + > rpm 201 device:201 all + > rpm 201 analogValue:1 all + > EOF + +The application prints content during interactive sessions. +""" + +import sys +import time +import json +from collections import OrderedDict + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser +from bacpypes.consolecmd import ConsoleCmd + +from bacpypes.pdu import Address, LocalBroadcast, GlobalBroadcast +from bacpypes.comm import bind +from bacpypes.core import run, deferred, enable_sleeping +from bacpypes.iocb import IOCB + +# application layer +from bacpypes.primitivedata import Unsigned, ObjectIdentifier +from bacpypes.constructeddata import Array +from bacpypes.basetypes import PropertyIdentifier +from bacpypes.object import get_datatype + +from bacpypes.app import ApplicationIOController +from bacpypes.appservice import ( + StateMachineAccessPoint, + ApplicationServiceAccessPoint, + ) +from bacpypes.apdu import ( + WhoIsRequest, + IAmRequest, + ReadPropertyRequest, + ReadPropertyACK, + ReadPropertyMultipleRequest, + PropertyReference, + ReadAccessSpecification, + ReadPropertyMultipleACK, + ) + +# network layer +from bacpypes.netservice import ( + NetworkServiceAccessPoint, + NetworkServiceElement, + ) +from bacpypes.npdu import ( + WhoIsRouterToNetwork, + IAmRouterToNetwork, + InitializeRoutingTable, + InitializeRoutingTableAck, + WhatIsNetworkNumber, + NetworkNumberIs, + ) + +# IPv4 virtual link layer +from bacpypes.bvllservice import BIPSimple, AnnexJCodec, UDPMultiplexer + +# basic objects +from bacpypes.local.device import LocalDeviceObject + +# basic services +from bacpypes.service.device import WhoIsIAmServices +from bacpypes.service.object import ReadWritePropertyServices + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +args = None +this_device = None +this_application = None +snapshot = None + +# print statements just for interactive +interactive = sys.stdin.isatty() + +# +# Snapshot +# + +@bacpypes_debugging +class Snapshot: + + def __init__(self): + if _debug: Snapshot._debug("__init__") + + # empty database + self.data = {} + + def read_file(self, filename): + if _debug: Snapshot._debug("read_file %r", filename) + + # empty database + self.data = {} + + try: + with open(filename) as infile: + lines = infile.readlines() + for line in lines: + devid, objid, propid, version, value = line[:-1].split('\t') + + devid = int(devid) + version = int(version) + + key = (devid, objid, propid) + self.data[key] = (version, value) + except IOError: + if _debug: Snapshot._debug(" - file not found") + pass + + def write_file(self, filename): + if _debug: Snapshot._debug("write_file %r", filename) + + data = list(k + v for k, v in self.data.items()) + data.sort() + + with open(filename, 'w') as outfile: + for row in data: + outfile.write('\t'.join(str(x) for x in row) + '\n') + + def upsert(self, devid, objid, propid, value): + if _debug: Snapshot._debug("upsert %r %r %r %r", devid, objid, propid, value) + + key = (devid, objid, propid) + if key not in self.data: + if _debug: Snapshot._debug(" - new key") + self.data[key] = (1, value) + else: + version, old_value = self.data[key] + if value != old_value: + if _debug: Snapshot._debug(" - new value") + self.data[key] = (version+1, value) + + def get_value(self, devid, objid, propid): + if _debug: Snapshot._debug("get_value %r %r %r", devid, objid, propid) + + key = (devid, objid, propid) + if key not in self.data: + return None + else: + return self.data[key][1] + +# +# DiscoverNetworkServiceElement +# + +@bacpypes_debugging +class DiscoverNetworkServiceElement(NetworkServiceElement): + + def __init__(self): + if _debug: DiscoverNetworkServiceElement._debug("__init__") + NetworkServiceElement.__init__(self) + + # no pending request + self._request = None + + def request(self, adapter, npdu): + if _debug: DiscoverNetworkServiceElement._debug("request %r %r", adapter, npdu) + + # save a copy of the request + self._request = npdu + + # forward it along + NetworkServiceElement.request(self, adapter, npdu) + + def indication(self, adapter, npdu): + if _debug: DiscoverNetworkServiceElement._debug("indication %r %r", adapter, npdu) + + if isinstance(npdu, IAmRouterToNetwork): + if interactive: + print("{} router to {}".format(npdu.pduSource, npdu.iartnNetworkList)) + + elif isinstance(npdu, InitializeRoutingTableAck): + if interactive: + print("{} routing table".format(npdu.pduSource)) + for rte in npdu.irtaTable: + print(" {} {} {}".format(rte.rtDNET, rte.rtPortID, rte.rtPortInfo)) + + elif isinstance(npdu, NetworkNumberIs): + if interactive: + print("{} network number is {}".format(npdu.pduSource, npdu.nniNet)) + + # forward it along + NetworkServiceElement.indication(self, adapter, npdu) + + def response(self, adapter, npdu): + if _debug: DiscoverNetworkServiceElement._debug("response %r %r", adapter, npdu) + + # forward it along + NetworkServiceElement.response(self, adapter, npdu) + + def confirmation(self, adapter, npdu): + if _debug: DiscoverNetworkServiceElement._debug("confirmation %r %r", adapter, npdu) + + # forward it along + NetworkServiceElement.confirmation(self, adapter, npdu) + +# +# DiscoverApplication +# + +@bacpypes_debugging +class DiscoverApplication(ApplicationIOController, WhoIsIAmServices, ReadWritePropertyServices): + + def __init__(self, localDevice, localAddress, deviceInfoCache=None, aseID=None): + if _debug: DiscoverApplication._debug("__init__ %r %r deviceInfoCache=%r aseID=%r", localDevice, localAddress, deviceInfoCache, aseID) + ApplicationIOController.__init__(self, localDevice, localAddress, deviceInfoCache, aseID=aseID) + + # local address might be useful for subclasses + if isinstance(localAddress, Address): + self.localAddress = localAddress + else: + self.localAddress = Address(localAddress) + + # 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 = DiscoverNetworkServiceElement() + bind(self.nse, self.nsap) + + # bind the top layers + bind(self, self.asap, self.smap, self.nsap) + + # create a generic BIP stack, bound to the Annex J server + # on the UDP multiplexer + self.bip = BIPSimple() + self.annexj = AnnexJCodec() + self.mux = UDPMultiplexer(self.localAddress) + + # bind the bottom layers + bind(self.bip, self.annexj, self.mux.annexJ) + + # bind the BIP stack to the network, no network number + self.nsap.bind(self.bip, address=self.localAddress) + + # keep track of requests to line up responses + self._request = None + + def close_socket(self): + if _debug: DiscoverApplication._debug("close_socket") + + # pass to the multiplexer, then down to the sockets + self.mux.close_socket() + + def request(self, apdu): + if _debug: DiscoverApplication._debug("request %r", apdu) + + # save a copy of the request + self._request = apdu + + # forward it along + super(DiscoverApplication, self).request(apdu) + + def indication(self, apdu): + if _debug: DiscoverApplication._debug("indication %r", apdu) + + # forward it along + super(DiscoverApplication, self).indication(apdu) + + def response(self, apdu): + if _debug: DiscoverApplication._debug("response %r", apdu) + + # forward it along + super(DiscoverApplication, self).response(apdu) + + def confirmation(self, apdu): + if _debug: DiscoverApplication._debug("confirmation %r", apdu) + + # forward it along + super(DiscoverApplication, self).confirmation(apdu) + + def do_IAmRequest(self, apdu): + if _debug: DiscoverApplication._debug("do_IAmRequest %r", apdu) + + if not isinstance(self._request, WhoIsRequest): + if _debug: DiscoverApplication._debug(" - no pending who-is") + return + + device_instance = apdu.iAmDeviceIdentifier[1] + if (self._request.deviceInstanceRangeLowLimit is not None) and \ + (device_instance < self._request.deviceInstanceRangeLowLimit): + return + if (self._request.deviceInstanceRangeHighLimit is not None) and \ + (device_instance > self._request.deviceInstanceRangeHighLimit): + return + + # print out something + if interactive: + print("{} @ {}".format(device_instance, apdu.pduSource)) + + # update the snapshot database + snapshot.upsert( + apdu.iAmDeviceIdentifier[1], '-', 'address', + str(apdu.pduSource), + ) + snapshot.upsert( + apdu.iAmDeviceIdentifier[1], '-', 'maxAPDULengthAccepted', + str(apdu.maxAPDULengthAccepted), + ) + snapshot.upsert( + apdu.iAmDeviceIdentifier[1], '-', 'segmentationSupported', + apdu.segmentationSupported, + ) + +# +# DiscoverConsoleCmd +# + +@bacpypes_debugging +class DiscoverConsoleCmd(ConsoleCmd): + + def do_wirtn(self, args): + """ + wirtn [ ] [ ] + + Send a Who-Is-Router-To-Network message. If is not specified + the message is locally broadcast. + """ + args = args.split() + if _debug: DiscoverConsoleCmd._debug("do_wirtn %r", args) + + # build a request + try: + request = WhoIsRouterToNetwork() + if not args: + request.pduDestination = LocalBroadcast() + elif args[0].isdigit(): + request.pduDestination = LocalBroadcast() + request.wirtnNetwork = int(args[0]) + else: + request.pduDestination = Address(args[0]) + if (len(args) > 1): + request.wirtnNetwork = int(args[1]) + except: + print("invalid arguments") + return + + # give it to the network service element + this_application.nse.request(this_application.nsap.local_adapter, request) + + # sleep for responses + time.sleep(3.0) + + def do_irt(self, args): + """ + irt + + Send an empty Initialize-Routing-Table message to an address, a router + will return an acknowledgement with its routing table configuration. + """ + args = args.split() + if _debug: DiscoverConsoleCmd._debug("do_irt %r", args) + + # build a request + try: + request = InitializeRoutingTable() + request.pduDestination = Address(args[0]) + except: + print("invalid arguments") + return + + # give it to the network service element + this_application.nse.request(this_application.nsap.local_adapter, request) + + def do_winn(self, args): + """ + winn [ ] + + Send a What-Is-Network-Number message. If the address is unspecified + the message is locally broadcast. + """ + args = args.split() + if _debug: DiscoverConsoleCmd._debug("do_winn %r", args) + + # build a request + try: + request = WhatIsNetworkNumber() + if (len(args) > 0): + request.pduDestination = Address(args[0]) + else: + request.pduDestination = LocalBroadcast() + except: + print("invalid arguments") + return + + # give it to the network service element + this_application.nse.request(this_application.nsap.local_adapter, request) + + # sleep for responses + time.sleep(3.0) + + def do_whois(self, args): + """ + whois [ ] [ ] + + Send a Who-Is Request and wait 3 seconds for the I-Am "responses" to + be returned. + """ + args = args.split() + if _debug: DiscoverConsoleCmd._debug("do_whois %r", args) + + try: + # build a request + request = WhoIsRequest() + if (len(args) == 1) or (len(args) == 3): + request.pduDestination = Address(args[0]) + del args[0] + else: + request.pduDestination = GlobalBroadcast() + + if len(args) == 2: + request.deviceInstanceRangeLowLimit = int(args[0]) + request.deviceInstanceRangeHighLimit = int(args[1]) + if _debug: DiscoverConsoleCmd._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) + if _debug: DiscoverConsoleCmd._debug(" - iocb: %r", iocb) + + # give it to the application + deferred(this_application.request_io, iocb) + + # sleep for responses + time.sleep(3.0) + + except Exception as err: + DiscoverConsoleCmd._exception("exception: %r", err) + + def do_iam(self, args): + """ + iam [ ] + + Send an I-Am request. If the address is unspecified the message is + locally broadcast. + """ + args = args.split() + if _debug: DiscoverConsoleCmd._debug("do_iam %r", args) + + try: + # build a request + request = IAmRequest() + if (len(args) == 1): + request.pduDestination = Address(args[0]) + else: + request.pduDestination = GlobalBroadcast() + + # set the parameters from the device object + request.iAmDeviceIdentifier = this_device.objectIdentifier + request.maxAPDULengthAccepted = this_device.maxApduLengthAccepted + request.segmentationSupported = this_device.segmentationSupported + request.vendorID = this_device.vendorIdentifier + if _debug: DiscoverConsoleCmd._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) + if _debug: DiscoverConsoleCmd._debug(" - iocb: %r", iocb) + + # give it to the application + this_application.request_io(iocb) + + except Exception as err: + DiscoverConsoleCmd._exception("exception: %r", err) + + def do_rp(self, args): + """ + rp [ ] + + Send a Read-Property request to a device identified by its device + identifier. + """ + args = args.split() + if _debug: DiscoverConsoleCmd._debug("do_rp %r", args) + + try: + devid, obj_id, prop_id = args[:3] + + devid = int(devid) + obj_id = ObjectIdentifier(obj_id).value + + datatype = get_datatype(obj_id[0], prop_id) + if not datatype: + raise ValueError("invalid property for object type") + + # map the devid identifier to an address from the database + addr = snapshot.get_value(devid, '-', 'address') + if not addr: + raise ValueError("unknown device") + if _debug: DiscoverConsoleCmd._debug(" - addr: %r", addr) + + # build a request + request = ReadPropertyRequest( + objectIdentifier=obj_id, + propertyIdentifier=prop_id, + ) + request.pduDestination = Address(addr) + + if len(args) == 4: + request.propertyArrayIndex = int(args[3]) + if _debug: DiscoverConsoleCmd._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) + if _debug: DiscoverConsoleCmd._debug(" - iocb: %r", iocb) + + # give it to the application + deferred(this_application.request_io, iocb) + + # wait for it to complete + iocb.wait() + + # do something for error/reject/abort + if iocb.ioError: + if interactive: + print(str(iocb.ioError)) + + # do something for success + elif iocb.ioResponse: + apdu = iocb.ioResponse + if _debug: DiscoverConsoleCmd._debug(" - apdu: %r", apdu) + + # should be an ack + if not isinstance(apdu, ReadPropertyACK): + if _debug: DiscoverConsoleCmd._debug(" - not an ack") + return + + # find the datatype + datatype = get_datatype(apdu.objectIdentifier[0], apdu.propertyIdentifier) + if _debug: DiscoverConsoleCmd._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 (apdu.propertyArrayIndex is not None): + if apdu.propertyArrayIndex == 0: + datatype = Unsigned + else: + datatype = datatype.subtype + if _debug: DiscoverConsoleCmd._debug(" - datatype: %r", datatype) + + value = apdu.propertyValue.cast_out(datatype) + if _debug: DiscoverConsoleCmd._debug(" - value: %r", value) + + # convert the value to a string + if hasattr(value, 'dict_contents'): + dict_contents = value.dict_contents(as_class=OrderedDict) + str_value = json.dumps(dict_contents) + else: + str_value = str(value) + if interactive: + print(str_value) + + # save it in the snapshot + snapshot.upsert(devid, '{}:{}'.format(*obj_id), prop_id, str_value) + + # do something with nothing? + else: + if _debug: DiscoverConsoleCmd._debug(" - ioError or ioResponse expected") + + except Exception as error: + DiscoverConsoleCmd._exception("exception: %r", error) + + def do_rpm(self, args): + """ + rpm ( ( [ ] )... )... + + Send a Read-Property-Multiple request to a device identified by its + device identifier. + """ + args = args.split() + if _debug: DiscoverConsoleCmd._debug("do_rpm %r", args) + + try: + i = 0 + devid = int(args[i]) + if _debug: DiscoverConsoleCmd._debug(" - devid: %r", devid) + i += 1 + + # map the devid identifier to an address from the database + addr = snapshot.get_value(devid, '-', 'address') + if not addr: + raise ValueError("unknown device") + if _debug: DiscoverConsoleCmd._debug(" - addr: %r", addr) + + read_access_spec_list = [] + while i < len(args): + obj_id = ObjectIdentifier(args[i]).value + if _debug: DiscoverConsoleCmd._debug(" - obj_id: %r", obj_id) + i += 1 + + prop_reference_list = [] + while i < len(args): + prop_id = args[i] + if _debug: DiscoverConsoleCmd._debug(" - prop_id: %r", prop_id) + if prop_id not in PropertyIdentifier.enumerations: + break + + i += 1 + if prop_id in ('all', 'required', 'optional'): + pass + else: + datatype = get_datatype(obj_id[0], prop_id) + if not datatype: + raise ValueError("invalid property for object type") + + # build a property reference + prop_reference = PropertyReference( + propertyIdentifier=prop_id, + ) + + # check for an array index + if (i < len(args)) and args[i].isdigit(): + prop_reference.propertyArrayIndex = int(args[i]) + i += 1 + + # add it to the list + prop_reference_list.append(prop_reference) + + # check for at least one property + if not prop_reference_list: + raise ValueError("provide at least one property") + + # build a read access specification + read_access_spec = ReadAccessSpecification( + objectIdentifier=obj_id, + listOfPropertyReferences=prop_reference_list, + ) + + # add it to the list + read_access_spec_list.append(read_access_spec) + + # check for at least one + if not read_access_spec_list: + raise RuntimeError("at least one read access specification required") + + # build the request + request = ReadPropertyMultipleRequest( + listOfReadAccessSpecs=read_access_spec_list, + ) + request.pduDestination = Address(addr) + if _debug: DiscoverConsoleCmd._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) + if _debug: DiscoverConsoleCmd._debug(" - iocb: %r", iocb) + + # give it to the application + deferred(this_application.request_io, iocb) + + # wait for it to complete + iocb.wait() + + # do something for success + if iocb.ioResponse: + apdu = iocb.ioResponse + if _debug: DiscoverConsoleCmd._debug(" - apdu: %r", apdu) + + # should be an ack + if not isinstance(apdu, ReadPropertyMultipleACK): + if _debug: DiscoverConsoleCmd._debug(" - not an ack") + return + + # loop through the results + for result in apdu.listOfReadAccessResults: + # here is the object identifier + objectIdentifier = result.objectIdentifier + if _debug: DiscoverConsoleCmd._debug(" - objectIdentifier: %r", objectIdentifier) + + # now come the property values per object + for element in result.listOfResults: + # get the property and array index + propertyIdentifier = element.propertyIdentifier + if _debug: DiscoverConsoleCmd._debug(" - propertyIdentifier: %r", propertyIdentifier) + propertyArrayIndex = element.propertyArrayIndex + if _debug: DiscoverConsoleCmd._debug(" - propertyArrayIndex: %r", propertyArrayIndex) + + # here is the read result + readResult = element.readResult + + property_label = str(propertyIdentifier) + if propertyArrayIndex is not None: + property_label += "[" + str(propertyArrayIndex) + "]" + + # check for an error + if readResult.propertyAccessError is not None: + if interactive: + print("{} ! {}".format(property_label, readResult.propertyAccessError)) + + else: + # here is the value + propertyValue = readResult.propertyValue + + # find the datatype + datatype = get_datatype(objectIdentifier[0], propertyIdentifier) + if _debug: DiscoverConsoleCmd._debug(" - datatype: %r", datatype) + if not datatype: + str_value = '?' + else: + # special case for array parts, others are managed by cast_out + if issubclass(datatype, Array) and (propertyArrayIndex is not None): + if propertyArrayIndex == 0: + datatype = Unsigned + else: + datatype = datatype.subtype + if _debug: DiscoverConsoleCmd._debug(" - datatype: %r", datatype) + + value = propertyValue.cast_out(datatype) + if _debug: DiscoverConsoleCmd._debug(" - value: %r", value) + + # convert the value to a string + if hasattr(value, 'dict_contents'): + dict_contents = value.dict_contents(as_class=OrderedDict) + str_value = json.dumps(dict_contents) + else: + str_value = str(value) + + if interactive: + print("{}: {}".format(property_label, str_value)) + + # save it in the snapshot + snapshot.upsert(devid, '{}:{}'.format(*objectIdentifier), property_label, str_value) + + # do something for error/reject/abort + if iocb.ioError: + if interactive: + print(str(iocb.ioError)) + + except Exception as error: + DiscoverConsoleCmd._exception("exception: %r", error) + +# +# __main__ +# + +def main(): + global args, this_device, this_application, snapshot + + # parse the command line arguments + parser = ConfigArgumentParser(description=__doc__) + + # input file for exiting configuration + parser.add_argument("infile", + default="-", + help="input file", + ) + + # output file for discovered configuration + parser.add_argument("outfile", nargs='?', + default='-unspecified-', + help="output file", + ) + + 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 = DiscoverApplication( + this_device, args.ini.address, + ) + if _debug: _log.debug(" - this_application: %r", this_application) + + # make a snapshot 'database' + snapshot = Snapshot() + + # read in an existing snapshot + if args.infile != '-': + snapshot.read_file(args.infile) + + # make a console + this_console = DiscoverConsoleCmd() + _log.debug(" - this_console: %r", this_console) + + # enable sleeping will help with threads + enable_sleeping() + + _log.debug("running") + + run() + + _log.debug("fini") + + # write out the snapshot, outfile defaults to infile if not specified + if args.outfile == '-unspecified-': + args.outfile = args.infile + if args.outfile != '-': + snapshot.write_file(args.outfile) + +if __name__ == "__main__": + main() diff --git a/samples/HTTPServer.py b/samples/HTTPServer.py index 84280f4c..f8c686dd 100755 --- a/samples/HTTPServer.py +++ b/samples/HTTPServer.py @@ -6,20 +6,24 @@ import threading import json +import zlib +from collections import OrderedDict from urlparse import urlparse, parse_qs + import SocketServer import SimpleHTTPServer from bacpypes.debugging import class_debugging, ModuleLogger from bacpypes.consolelogging import ConfigArgumentParser -from bacpypes.core import run +from bacpypes.core import run, deferred from bacpypes.iocb import IOCB from bacpypes.pdu import Address, GlobalBroadcast from bacpypes.apdu import ReadPropertyRequest, WhoIsRequest -from bacpypes.primitivedata import ObjectIdentifier +from bacpypes.primitivedata import Unsigned, ObjectIdentifier +from bacpypes.constructeddata import Array from bacpypes.app import BIPSimpleApplication from bacpypes.object import get_object_class, get_datatype @@ -33,6 +37,29 @@ this_application = None server = None +# favorite icon +favicon = zlib.decompress( +'x\x9c\xb5\x93\xcdN\xdb@\x14\x85\x07\x95\x07\xc8\x8amYv\xc9#\xe4\x11x\x04\x96}' +'\x8c\x88\x1dl\xa0\x9b\xb6A\xa2)\x0bVTB\xa9"\xa5?*I\x16\xad"\x84d\x84DE\x93' +'\x14;v\xc01M\xe2$\x988\xb1l\x9d\xde;v\\\x03\x89TU\xea\xb5N\xe4\xb9\x9a\xef' +'\x1c\xcfO\x84X\xa0\'\x95\x12\xf4\xbb,\x9e/\n\xb1$\x84xF\xa2\x16u\xc2>WzQ\xfc' +'\xf7\xca\xad\xafo\x91T\xd2\x1ai\xe5\x1fx[\xf9\xf4\x01\xc57\xbb\xd8\xdf\xd8' +'\x00\x8d\x11\xf9\x95\x12\xda\x9a\xc3\xae\xe5_\xbdDpk\x03\xc3\xaeT\xd0\xb3\xd0' +'>?\x83Z\xfd\x86Z\xa5\x84\x1fG_\xa4\xe7\x1c^\xa9W\xbfJ\xfe\xb4\xf0\x0e^\xdb' +'\x88}0 \xafA\x0f\xa3+c&O\xbd\xf4\xc1\xf6\xb6d\x9d\xc6\x05\xdcVSz\xb0x\x1c\x10' +'\x0fo\x02\xc7\xd0\xe7\xf1%\xe5\xf3\xc78\xdb\xf9Y\x93\x1eI\x1f\xf8>\xfa\xb5' +'\x8bG<\x8dW\x0f^\x84\xd9\xee\xb5~\x8f\xe1w\xaf{\x83\x80\xb2\xbd\xe1\x10\x83' +'\x88\'\xa5\x12\xbcZ?9\x8e\xb3%\xd3\xeb`\xd4\xd2\xffdS\xb9\x96\x89!}W!\xfb\x9a' +'\xf9t\xc4f\x8aos\x92\x9dtn\xe0\xe8Z\xcc\xc8=\xec\xf7d6\x97\xa3]\xc2Q\x1b(\xec' +'d\x99_\x8dx\xd4\x15%\xce\x96\xf9\xbf\xacP\xd1:\xfc\xf1\x18\xbe\xeb\xe2\xaey' +'\x89;]\xc5\xf1\xfb<\xf3\x99\xe9\x99\xefon\xa2\xdb6\xe5\x1c\xbb^\x8b}FV\x1b' +'\x9es+\xb3\xbd\x81M\xeb\xd1\xe0^5\xf1\xbd|\xc4\xfca\xf2\xde\xf0w\x9cW\xabr.' +'\xe7\xd9\x8dFx\x0e\xa6){\x93\x8e\x85\xf1\xb5\x81\x89\xd9\x82\xa1\x9c\xc8;\xf9' +'\xe0\x0cV\xb8W\xdc\xdb\x83\xa9i\xb1O@g\xa6T*\xd3=O\xeaP\xcc(^\x17\xfb\xe4\xb3' +'Y\xc9\xb1\x17{N\xf7\xfbo\x8b\xf7\x97\x94\xe3;\xcd\xff)\xd2\xf2\xacy\xa0\x9b' +'\xd4g=\x11B\x8bT\x8e\x94Y\x08%\x12\xe2q\x99\xd4\x7f*\x84O\xfa\r\xb5\x916R' +) + # # ThreadedHTTPRequestHandler # @@ -42,6 +69,7 @@ class ThreadedHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler): def do_GET(self): if _debug: ThreadedHTTPRequestHandler._debug("do_GET") + global favicon # get the thread cur_thread = threading.current_thread() @@ -61,6 +89,14 @@ def do_GET(self): self.do_read(args[2:]) elif (args[1] == 'whois'): self.do_whois(args[2:]) + if args[1] == 'favicon.ico': + self.send_response(200) + self.send_header("Content-type", 'image/x-icon') + self.send_header("Content-Length", len(favicon)) + self.end_headers() + self.wfile.write(favicon) + else: + return "'read' or 'whois' expected" def do_read(self, args): if _debug: ThreadedHTTPRequestHandler._debug("do_read %r", args) @@ -102,7 +138,7 @@ def do_read(self, args): if _debug: ThreadedHTTPRequestHandler._debug(" - iocb: %r", iocb) # give it to the application - this_application.request_io(iocb) + deferred(this_application.request_io, iocb) # wait for it to complete iocb.wait() @@ -113,7 +149,29 @@ def do_read(self, args): result = { "error": str(iocb.ioError) } else: if _debug: ThreadedHTTPRequestHandler._debug(" - response: %r", iocb.ioResponse) - result = { "value": iocb.ioResponse } + apdu = iocb.ioResponse + + # find the datatype + datatype = get_datatype(apdu.objectIdentifier[0], apdu.propertyIdentifier) + if _debug: ThreadedHTTPRequestHandler._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 (apdu.propertyArrayIndex is not None): + if apdu.propertyArrayIndex == 0: + datatype = Unsigned + else: + datatype = datatype.subtype + if _debug: ThreadedHTTPRequestHandler._debug(" - datatype: %r", datatype) + + # convert the value to a dict if possible + value = apdu.propertyValue.cast_out(datatype) + if hasattr(value, 'dict_contents'): + value = value.dict_contents(as_class=OrderedDict) + if _debug: ThreadedHTTPRequestHandler._debug(" - value: %r", value) + + result = { "value": value } except Exception as err: ThreadedHTTPRequestHandler._exception("exception: %r", err) diff --git a/samples/IP2VLANRouter.py b/samples/IP2VLANRouter.py index 8fdbfd1e..da58c118 100755 --- a/samples/IP2VLANRouter.py +++ b/samples/IP2VLANRouter.py @@ -25,11 +25,17 @@ from bacpypes.app import Application from bacpypes.appservice import StateMachineAccessPoint, ApplicationServiceAccessPoint from bacpypes.local.device import LocalDeviceObject -from bacpypes.service.device import WhoIsIAmServices -from bacpypes.service.object import ReadWritePropertyServices +from bacpypes.local.object import CurrentPropertyList +from bacpypes.service.device import ( + WhoIsIAmServices, + ) +from bacpypes.service.object import ( + ReadWritePropertyServices, + ReadWritePropertyMultipleServices, + ) from bacpypes.primitivedata import Real -from bacpypes.object import AnalogValueObject, Property +from bacpypes.object import register_object_type, AnalogValueObject, Property from bacpypes.vlan import Network, Node from bacpypes.errors import ExecutionError @@ -38,6 +44,9 @@ _debug = 0 _log = ModuleLogger(globals()) +# globals +args = None + # # RandomValueProperty # @@ -60,11 +69,21 @@ def ReadProperty(self, obj, arrayIndex=None): value = random.random() * 100.0 if _debug: RandomValueProperty._debug(" - value: %r", value) + # save the value that was generated + super(RandomValueProperty, self).WriteProperty(obj, value, direct=True) + + # now return it to the client return value def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): if _debug: RandomValueProperty._debug("WriteProperty %r %r arrayIndex=%r priority=%r direct=%r", obj, value, arrayIndex, priority, direct) - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + if not direct: + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # continue along + super(RandomValueProperty, self).WriteProperty(obj, value, direct=True) # # Random Value Object Type @@ -81,16 +100,31 @@ def __init__(self, **kwargs): if _debug: RandomAnalogValueObject._debug("__init__ %r", kwargs) AnalogValueObject.__init__(self, **kwargs) + # if a value hasn't already been provided, initialize with a random one + if 'presentValue' not in kwargs: + self.presentValue = random.random() * 100.0 + # # VLANApplication # @bacpypes_debugging -class VLANApplication(Application, WhoIsIAmServices, ReadWritePropertyServices): +class VLANApplication( + Application, + WhoIsIAmServices, + ReadWritePropertyServices, + ): def __init__(self, vlan_device, vlan_address, aseID=None): if _debug: VLANApplication._debug("__init__ %r %r aseID=%r", vlan_device, vlan_address, aseID) - Application.__init__(self, vlan_device, vlan_address, aseID) + global args + + # normal initialization + Application.__init__(self, vlan_device, aseID=aseID) + + # optional read property multiple + if args.rpm: + self.add_capability(ReadWritePropertyMultipleServices) # include a application decoder self.asap = ApplicationServiceAccessPoint() @@ -169,6 +203,8 @@ def __init__(self, local_address, local_network): # def main(): + global args + # parse the command line arguments parser = ArgumentParser( description=__doc__, @@ -177,24 +213,36 @@ def main(): # add an argument for interval parser.add_argument('addr1', type=str, - help='address of first network', - ) + help='address of first network', + ) # add an argument for interval parser.add_argument('net1', type=int, - help='network number of first network', - ) + help='network number of first network', + ) # add an argument for interval parser.add_argument('net2', type=int, - help='network number of second network', - ) + help='network number of second network', + ) # add an argument for how many virtual devices parser.add_argument('--count', type=int, - help='number of virtual devices', - default=1, - ) + help='number of virtual devices', + default=1, + ) + + # add an argument for how many virtual devices + parser.add_argument('--rpm', + help='enable read property multiple', + action="store_true", + ) + + # add an argument for including the property list + parser.add_argument('--plist', + help='enable property list property', + action="store_true", + ) # now parse the arguments args = parser.parse_args() @@ -222,6 +270,13 @@ def main(): # send network topology deferred(router.nse.i_am_router_to_network) + # add the dynamic property list + if args.plist: + RandomAnalogValueObject.properties.append(CurrentPropertyList()) + + # register it now that all its properties are defined + register_object_type(RandomAnalogValueObject, vendor_id=999) + # make some devices for device_number in range(2, 2 + args.count): # device identifier is assigned from the address diff --git a/samples/MicroPython/docs/SonoffESP8266_notes.txt b/samples/MicroPython/docs/SonoffESP8266_notes.txt new file mode 100644 index 00000000..ec552e72 --- /dev/null +++ b/samples/MicroPython/docs/SonoffESP8266_notes.txt @@ -0,0 +1,52 @@ +Install Notes for Flashing MicroPython on Sonoff WiFi Switch + +**Read these instructions first. They help with FTDI connections. +https://medium.com/cloud4rpi/getting-micropython-on-a-sonoff-smart-switch-1df6c071720a + +**You will need a FDTI USB adapter. The one I used: +HiLetgo FT232RL FTDI Mini USB to TTL Serial Converter Adapter Module +Got it from Amazon. + + +1. download esptool -> $ pip install esptool + +2. download latest micropython firmware -> http://micropython.org/download + Current version - esp8266-20190125-v1.10.bin + +3. With power disconnected, hold down the button, while holing down the button, + plug in the usb cable to the computer and continue to hold for 3 seconds. This puts the device in flash mode. + + ** line voltage to the sonoff is not necessary for any step in this process. The device will be powered from + the 3.3v of the FTDI adapter. + +4. $ esptool.py --port [USB adapter com port] erase_flash + in my case: esptool.py --port COM22 erase_flash + +5. After erase is completed, unplug usb from computer, and put the device in flash mode again. + +6. $ esptool.py --port [USB adapter com port] write_flash -fs 1MB -fm dout 0x0 [path to firmware .bin file] + in my case: esptool.py --port COM22 write_flash -fs 1MB -fm dout 0x0 C:\uPyCraft\esp8266-20190125-v1.10.bin + + ^^ THIS ONE IS IMPORTANT ^^ + All the instructions I found online use a different command to install the firmware but because of the manufacurer + of the chip used by Sonoff the command below is the correct one to use. + + + + +You should be cooking with propane now! + +Other Helpfull Notes: + +- Micropython for esp8266 Docs: + http://docs.micropython.org/en/latest/esp8266/quickref.html + +- uPyCraft IDE for MicroPython: + ** I was not able to get the firmware update feature of this IDE to work. Most likely because of the special esptool command + needed to flash firmware. + + https://randomnerdtutorials.com/install-upycraft-ide-windows-pc-instructions/ + If you dont trust their download: + https://github.com/DFRobot/uPyCraft + + diff --git a/samples/MultipleReadPropertyThreaded.py b/samples/MultipleReadPropertyThreaded.py index 8da52a2b..a659da23 100755 --- a/samples/MultipleReadPropertyThreaded.py +++ b/samples/MultipleReadPropertyThreaded.py @@ -75,7 +75,7 @@ def run(self): if _debug: ReadPointListThread._debug(" - iocb: %r", iocb) # give it to the application - this_application.request_io(iocb) + deferred(this_application.request_io, iocb) # wait for the response iocb.wait() diff --git a/samples/RaspberryPi/binaryio.py b/samples/RaspberryPi/binaryio.py new file mode 100644 index 00000000..7efc6c22 --- /dev/null +++ b/samples/RaspberryPi/binaryio.py @@ -0,0 +1,268 @@ +#!/usr/bin/env python + +""" +Raspberry Pi Binary Input and Output +This sample application is BACnet server running on a Rasbperry Pi that +associates a Button with a BinaryInputObject and a LED with a +BinaryOutputObject. +""" + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser + +from bacpypes.core import run + +from bacpypes.basetypes import BinaryPV +from bacpypes.object import ( + BinaryInputObject, + BinaryOutputObject, + Property, + register_object_type, +) +from bacpypes.errors import ExecutionError + +from bacpypes.app import BIPSimpleApplication +from bacpypes.local.device import LocalDeviceObject + +from gpiozero import Button, LED + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# button number, binary input object instance number +button_list = [(2, 2)] + +# LED number, binary output object instance number +led_list = [(17, 17)] + +# +# Mock classes +# + +''' +# We may want to look into the mock pin class of gpiozero. +# It will allow for testing without using an RPI board. + +class Button: + def __init__(self, button_id): + pass + + +class LED: + def __init__(self, led_id): + pass +''' + +# +# BIPresentValue +# + + +@bacpypes_debugging +class BIPresentValue(Property): + def __init__(self, identifier): + if _debug: + BIPresentValue._debug("__init__ %r", identifier) + Property.__init__( + self, identifier, BinaryPV, default=False, optional=False, mutable=False + ) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: + BIPresentValue._debug("ReadProperty %r arrayIndex=%r", obj, arrayIndex) + + # access an array + if arrayIndex is not None: + raise ExecutionError( + errorClass="property", errorCode="propertyIsNotAnArray" + ) + + + ###TODO: obj._button is the Button object + + if _debug: + BIPresentValue._debug(" - read button: %r", obj._button) + + if obj._button.is_pressed: + return "active" + + else: + return "inactive" + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + if _debug: + BIPresentValue._debug( + "WriteProperty %r %r arrayIndex=%r priority=%r direct=%r", + obj, + value, + arrayIndex, + priority, + direct, + ) + + raise ExecutionError(errorClass="property", errorCode="writeAccessDenied") + + +# +# RPiBinaryInput +# + + +@bacpypes_debugging +@register_object_type +class RPiBinaryInput(BinaryInputObject): + + properties = [BIPresentValue("presentValue")] + + def __init__(self, button_id, **kwargs): + if _debug: + RPiBinaryInput._debug("__init__ %r %r", button_id, kwargs) + BinaryInputObject.__init__(self, **kwargs) + + # create a button object + self._button = Button(button_id) + + +# +# BOPresentValue +# + + +@bacpypes_debugging +class BOPresentValue(Property): + def __init__(self, identifier): + if _debug: + BOPresentValue._debug("__init__ %r", identifier) + Property.__init__( + self, identifier, BinaryPV, default=False, optional=False, mutable=True + ) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: + BOPresentValue._debug("ReadProperty %r arrayIndex=%r", obj, arrayIndex) + + # access an array + if arrayIndex is not None: + raise ExecutionError( + errorClass="property", errorCode="propertyIsNotAnArray" + ) + + + ###TODO: obj._led is the LED object + if _debug: + BOPresentValue._debug(" - read led: %r", obj._led) + + if obj._led.value == 1: + return "active" + else: + return "inactive" + + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + if _debug: + BOPresentValue._debug( + "WriteProperty %r %r arrayIndex=%r priority=%r direct=%r", + obj, + value, + arrayIndex, + priority, + direct, + ) + + # access an array + if arrayIndex is not None: + raise ExecutionError( + errorClass="property", errorCode="propertyIsNotAnArray" + ) + + ###TODO: obj._button is the Button object + if _debug: + BOPresentValue._debug(" - write led: %r", obj._led) + + #raise ExecutionError(errorClass="property", errorCode="writeAccessDenied") + + if value == "active": + obj._led.on() + elif value == "inactive": + obj._led.off() + else: + ### TODO: insert correct value error. Below is a placeholder. + print("invalid value for led. Use 'active' to turn on or 'inactive' to turn off.") + + +# +# RPiBinaryOutput +# + + +@bacpypes_debugging +@register_object_type +class RPiBinaryOutput(BinaryOutputObject): + + properties = [BOPresentValue("presentValue")] + + def __init__(self, led_id, **kwargs): + if _debug: + RPiBinaryOutput._debug("__init__ %r %r", led_id, kwargs) + BinaryOutputObject.__init__(self, **kwargs) + + # make an LED object + self._led = LED(led_id) + + +# +# __main__ +# + + +def main(): + # 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( + objectName=args.ini.objectname, + objectIdentifier=("device", int(args.ini.objectidentifier)), + maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), + segmentationSupported=args.ini.segmentationsupported, + vendorIdentifier=int(args.ini.vendoridentifier), + ) + + # make a sample application + this_application = BIPSimpleApplication(this_device, args.ini.address) + + # make the buttons + for button_id, bio_id in button_list: + bio = RPiBinaryInput( + button_id, + objectIdentifier=("binaryInput", bio_id), + objectName="Button-%d" % (button_id,), + ) + _log.debug(" - bio: %r", bio) + this_application.add_object(bio) + + # make the LEDs + for led_id, boo_id in led_list: + boo = RPiBinaryOutput( + led_id, + objectIdentifier=("binaryOutput", boo_id), + objectName="LED-%d" % (led_id,), + ) + _log.debug(" - boo: %r", boo) + this_application.add_object(boo) + + _log.debug("running") + + run() + + _log.debug("fini") + + +if __name__ == "__main__": + main() diff --git a/samples/ReadProperty.py b/samples/ReadProperty.py index a9de3c3f..ad1072ee 100755 --- a/samples/ReadProperty.py +++ b/samples/ReadProperty.py @@ -12,7 +12,7 @@ from bacpypes.consolelogging import ConfigArgumentParser from bacpypes.consolecmd import ConsoleCmd -from bacpypes.core import run, enable_sleeping +from bacpypes.core import run, deferred, enable_sleeping from bacpypes.iocb import IOCB from bacpypes.pdu import Address @@ -40,7 +40,7 @@ class ReadPropertyConsoleCmd(ConsoleCmd): def do_read(self, args): - """read [ ]""" + """read [ ]""" args = args.split() if _debug: ReadPropertyConsoleCmd._debug("do_read %r", args) @@ -68,7 +68,7 @@ def do_read(self, args): if _debug: ReadPropertyConsoleCmd._debug(" - iocb: %r", iocb) # give it to the application - this_application.request_io(iocb) + deferred(this_application.request_io, iocb) # wait for it to complete iocb.wait() diff --git a/samples/ReadProperty25.py b/samples/ReadProperty25.py index da74b1e5..b7b54501 100755 --- a/samples/ReadProperty25.py +++ b/samples/ReadProperty25.py @@ -12,16 +12,16 @@ from bacpypes.consolelogging import ConfigArgumentParser from bacpypes.consolecmd import ConsoleCmd -from bacpypes.core import run, enable_sleeping +from bacpypes.core import run, deferred, enable_sleeping from bacpypes.iocb import IOCB from bacpypes.pdu import Address from bacpypes.apdu import ReadPropertyRequest, ReadPropertyACK -from bacpypes.primitivedata import Unsigned +from bacpypes.primitivedata import Unsigned, ObjectIdentifier from bacpypes.constructeddata import Array from bacpypes.app import BIPSimpleApplication -from bacpypes.object import get_object_class, get_datatype +from bacpypes.object import get_datatype from bacpypes.local.device import LocalDeviceObject # some debugging @@ -39,33 +39,27 @@ class ReadPropertyConsoleCmd(ConsoleCmd): def do_read(self, args): - """read [ ]""" + """read [ ]""" args = args.split() if _debug: ReadPropertyConsoleCmd._debug("do_read %r", args) try: - addr, obj_type, obj_inst, prop_id = args[:4] + addr, obj_id, prop_id = args[:3] + obj_id = ObjectIdentifier(obj_id).value - if obj_type.isdigit(): - obj_type = int(obj_type) - elif not get_object_class(obj_type): - raise ValueError("unknown object type") - - obj_inst = int(obj_inst) - - datatype = get_datatype(obj_type, prop_id) + datatype = get_datatype(obj_id[0], prop_id) if not datatype: raise ValueError("invalid property for object type") # build a request request = ReadPropertyRequest( - objectIdentifier=(obj_type, obj_inst), + objectIdentifier=obj_id, propertyIdentifier=prop_id, ) request.pduDestination = Address(addr) - if len(args) == 5: - request.propertyArrayIndex = int(args[4]) + if len(args) == 4: + request.propertyArrayIndex = int(args[3]) if _debug: ReadPropertyConsoleCmd._debug(" - request: %r", request) # make an IOCB @@ -73,7 +67,7 @@ def do_read(self, args): if _debug: ReadPropertyConsoleCmd._debug(" - iocb: %r", iocb) # give it to the application - this_application.request_io(iocb) + deferred(this_application.request_io, iocb) # wait for it to complete iocb.wait() diff --git a/samples/ReadPropertyAny.py b/samples/ReadPropertyAny.py index a14cf762..c429c8fd 100755 --- a/samples/ReadPropertyAny.py +++ b/samples/ReadPropertyAny.py @@ -13,13 +13,14 @@ from bacpypes.consolelogging import ConfigArgumentParser from bacpypes.consolecmd import ConsoleCmd -from bacpypes.core import run, enable_sleeping +from bacpypes.core import run, deferred, enable_sleeping from bacpypes.iocb import IOCB from bacpypes.pdu import Address from bacpypes.apdu import ReadPropertyRequest from bacpypes.primitivedata import Tag, ObjectIdentifier +from bacpypes.constructeddata import ArrayOf from bacpypes.app import BIPSimpleApplication from bacpypes.local.device import LocalDeviceObject @@ -64,7 +65,7 @@ def do_read(self, args): if _debug: ReadPropertyAnyConsoleCmd._debug(" - iocb: %r", iocb) # give it to the application - this_application.request_io(iocb) + deferred(this_application.request_io, iocb) # wait for it to complete iocb.wait() @@ -72,27 +73,40 @@ def do_read(self, args): # do something for success if iocb.ioResponse: apdu = iocb.ioResponse + if _debug: ReadPropertyAnyConsoleCmd._debug(" - apdu: %r", apdu) - # peek at the value tag - value_tag = apdu.propertyValue.tagList.Peek() - if _debug: ReadPropertyAnyConsoleCmd._debug(" - value_tag: %r", value_tag) + try: + tag_list = apdu.propertyValue.tagList - # make sure that it is application tagged - if value_tag.tagClass != Tag.applicationTagClass: - sys.stdout.write("value is not application encoded\n") + # all tags application encoded + non_app_tags = [tag for tag in tag_list if tag.tagClass != Tag.applicationTagClass] + if non_app_tags: + raise RuntimeError("value has some non-application tags") + + # all the same type + first_tag = tag_list[0] + other_type_tags = [tag for tag in tag_list[1:] if tag.tagNumber != first_tag.tagNumber] + if other_type_tags: + raise RuntimeError("all the tags must be the same type") - else: # find the datatype - datatype = Tag._app_tag_class[value_tag.tagNumber] + datatype = Tag._app_tag_class[first_tag.tagNumber] if _debug: ReadPropertyAnyConsoleCmd._debug(" - datatype: %r", datatype) if not datatype: - raise TypeError("unknown datatype") + raise RuntimeError("unknown datatype") + + # more than one then it's an array of these + if len(tag_list) > 1: + datatype = ArrayOf(datatype) + if _debug: ReadPropertyAnyConsoleCmd._debug(" - array: %r", datatype) # cast out the value value = apdu.propertyValue.cast_out(datatype) if _debug: ReadPropertyAnyConsoleCmd._debug(" - value: %r", value) sys.stdout.write("%s (%s)\n" % (value, datatype)) + except RuntimeError as err: + sys.stdout.write("error: %s\n" % (err,)) sys.stdout.flush() diff --git a/samples/ReadPropertyForeign.py b/samples/ReadPropertyForeign.py new file mode 100755 index 00000000..5ced6139 --- /dev/null +++ b/samples/ReadPropertyForeign.py @@ -0,0 +1,178 @@ +#!/usr/bin/env python + +""" +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. + +In addition to the usual INI parameters that are common to BACpypes applications, +this application references two additional parameters: + + foreignBBMD: the BACpypes IP Address of the BBMD to register + foreignTTL: the time-to-live to keep the registration alive + +The BBMDForeign class will send the BVLL registration request after the core +starts up and maintain it. If the device does not get an 'ack' then it will +not send requests, even to devices that it would be able to talk otherwise. +""" + +import sys + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser +from bacpypes.consolecmd import ConsoleCmd + +from bacpypes.core import run, deferred, enable_sleeping +from bacpypes.iocb import IOCB + +from bacpypes.pdu import Address +from bacpypes.apdu import ReadPropertyRequest, ReadPropertyACK +from bacpypes.primitivedata import Unsigned, ObjectIdentifier +from bacpypes.constructeddata import Array + +from bacpypes.app import BIPForeignApplication +from bacpypes.object import get_datatype +from bacpypes.local.device import LocalDeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +this_application = None + + +# +# ReadPropertyConsoleCmd +# + +@bacpypes_debugging +class ReadPropertyConsoleCmd(ConsoleCmd): + + def do_read(self, args): + """read [ ]""" + args = args.split() + if _debug: ReadPropertyConsoleCmd._debug("do_read %r", args) + + try: + addr, obj_id, prop_id = args[:3] + obj_id = ObjectIdentifier(obj_id).value + + datatype = get_datatype(obj_id[0], prop_id) + if not datatype: + raise ValueError("invalid property for object type") + + # build a request + request = ReadPropertyRequest( + objectIdentifier=obj_id, + propertyIdentifier=prop_id, + ) + request.pduDestination = Address(addr) + + if len(args) == 4: + request.propertyArrayIndex = int(args[3]) + if _debug: ReadPropertyConsoleCmd._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) + if _debug: ReadPropertyConsoleCmd._debug(" - iocb: %r", iocb) + + # give it to the application + deferred(this_application.request_io, iocb) + + # wait for it to complete + iocb.wait() + + # do something for error/reject/abort + if iocb.ioError: + sys.stdout.write(str(iocb.ioError) + '\n') + + # do something for success + elif iocb.ioResponse: + apdu = iocb.ioResponse + + # should be an ack + if not isinstance(apdu, ReadPropertyACK): + if _debug: ReadPropertyConsoleCmd._debug(" - not an ack") + return + + # find the datatype + datatype = get_datatype(apdu.objectIdentifier[0], apdu.propertyIdentifier) + if _debug: ReadPropertyConsoleCmd._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 (apdu.propertyArrayIndex is not None): + if apdu.propertyArrayIndex == 0: + value = apdu.propertyValue.cast_out(Unsigned) + else: + value = apdu.propertyValue.cast_out(datatype.subtype) + else: + value = apdu.propertyValue.cast_out(datatype) + if _debug: ReadPropertyConsoleCmd._debug(" - value: %r", value) + + sys.stdout.write(str(value) + '\n') + if hasattr(value, 'debug_contents'): + value.debug_contents(file=sys.stdout) + sys.stdout.flush() + + # do something with nothing? + else: + if _debug: ReadPropertyConsoleCmd._debug(" - ioError or ioResponse expected") + + except Exception as error: + ReadPropertyConsoleCmd._exception("exception: %r", error) + + def do_rtn(self, args): + """rtn ... """ + args = args.split() + if _debug: ReadPropertyConsoleCmd._debug("do_rtn %r", args) + + # provide the address and a list of network numbers + router_address = Address(args[0]) + network_list = [int(arg) for arg in args[1:]] + + # pass along to the service access point + this_application.nsap.add_router_references(None, router_address, network_list) + + +# +# __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 = BIPForeignApplication( + this_device, args.ini.address, + Address(args.ini.foreignbbmd), + int(args.ini.foreignttl), + ) + + # make a console + this_console = ReadPropertyConsoleCmd() + if _debug: _log.debug(" - this_console: %r", this_console) + + # enable sleeping will help with threads + enable_sleeping() + + _log.debug("running") + + run() + + _log.debug("fini") + +if __name__ == "__main__": + main() diff --git a/samples/ReadPropertyMultiple.py b/samples/ReadPropertyMultiple.py index b47bf447..b60d65b4 100755 --- a/samples/ReadPropertyMultiple.py +++ b/samples/ReadPropertyMultiple.py @@ -12,7 +12,7 @@ from bacpypes.consolelogging import ConfigArgumentParser from bacpypes.consolecmd import ConsoleCmd -from bacpypes.core import run, enable_sleeping +from bacpypes.core import run, deferred, enable_sleeping from bacpypes.iocb import IOCB from bacpypes.pdu import Address @@ -112,7 +112,7 @@ def do_read(self, args): if _debug: ReadPropertyMultipleConsoleCmd._debug(" - iocb: %r", iocb) # give it to the application - this_application.request_io(iocb) + deferred(this_application.request_io, iocb) # wait for it to complete iocb.wait() diff --git a/samples/ReadPropertyMultiple25.py b/samples/ReadPropertyMultiple25.py index ab712e6d..13a2c9f4 100755 --- a/samples/ReadPropertyMultiple25.py +++ b/samples/ReadPropertyMultiple25.py @@ -12,15 +12,15 @@ from bacpypes.consolelogging import ConfigArgumentParser from bacpypes.consolecmd import ConsoleCmd -from bacpypes.core import run, enable_sleeping +from bacpypes.core import run, deferred, enable_sleeping from bacpypes.iocb import IOCB from bacpypes.pdu import Address -from bacpypes.object import get_object_class, get_datatype +from bacpypes.object import get_datatype from bacpypes.apdu import ReadPropertyMultipleRequest, PropertyReference, \ ReadAccessSpecification, ReadPropertyMultipleACK -from bacpypes.primitivedata import Unsigned +from bacpypes.primitivedata import Unsigned, ObjectIdentifier from bacpypes.constructeddata import Array from bacpypes.basetypes import PropertyIdentifier @@ -41,7 +41,7 @@ class ReadPropertyMultipleConsoleCmd(ConsoleCmd): def do_read(self, args): - """read ( ( [ ] )... )...""" + """read ( ( [ ] )... )...""" args = args.split() if _debug: ReadPropertyMultipleConsoleCmd._debug("do_read %r", args) @@ -52,15 +52,7 @@ def do_read(self, args): read_access_spec_list = [] while i < len(args): - obj_type = args[i] - i += 1 - - if obj_type.isdigit(): - obj_type = int(obj_type) - elif not get_object_class(obj_type): - raise ValueError("unknown object type") - - obj_inst = int(args[i]) + obj_id = ObjectIdentifier(args[i]).value i += 1 prop_reference_list = [] @@ -73,7 +65,7 @@ def do_read(self, args): if prop_id in ('all', 'required', 'optional'): pass else: - datatype = get_datatype(obj_type, prop_id) + datatype = get_datatype(obj_id[0], prop_id) if not datatype: raise ValueError("invalid property for object type") @@ -96,7 +88,7 @@ def do_read(self, args): # build a read access specification read_access_spec = ReadAccessSpecification( - objectIdentifier=(obj_type, obj_inst), + objectIdentifier=obj_id, listOfPropertyReferences=prop_reference_list, ) @@ -119,7 +111,7 @@ def do_read(self, args): if _debug: ReadPropertyMultipleConsoleCmd._debug(" - iocb: %r", iocb) # give it to the application - this_application.request_io(iocb) + deferred(this_application.request_io, iocb) # wait for it to complete iocb.wait() diff --git a/samples/ReadPropertyMultipleServer.py b/samples/ReadPropertyMultipleServer.py index 13c9d7df..595aca70 100755 --- a/samples/ReadPropertyMultipleServer.py +++ b/samples/ReadPropertyMultipleServer.py @@ -106,7 +106,7 @@ def main(): # make a random input object ravo1 = RandomAnalogValueObject( objectIdentifier=('analogValue', 1), objectName='Random1', - eventMessageTexts=ArrayOf(CharacterString)(["hello"]), + eventMessageTexts=ArrayOf(CharacterString)(["to", "infinity", "and", "beyond"]), ) _log.debug(" - ravo1: %r", ravo1) diff --git a/samples/ReadRange.py b/samples/ReadRange.py index 0eca99b7..b1373273 100755 --- a/samples/ReadRange.py +++ b/samples/ReadRange.py @@ -1,9 +1,9 @@ #!/usr/bin/env python """ -This application presents a 'console' prompt to the user asking for readrange commands -which create ReadRangeRequest PDUs, then lines up the coorresponding ReadRangeACK -and prints the value. +This application presents a 'console' prompt to the user asking for readrange +commands which create ReadRangeRequest PDUs, then lines up the coorresponding +ReadRangeACK and prints the value. """ import sys @@ -12,15 +12,24 @@ from bacpypes.consolelogging import ConfigArgumentParser from bacpypes.consolecmd import ConsoleCmd -from bacpypes.core import run, enable_sleeping +from bacpypes.core import run, deferred, enable_sleeping from bacpypes.iocb import IOCB from bacpypes.pdu import Address from bacpypes.object import get_datatype -from bacpypes.apdu import ReadRangeRequest, ReadRangeACK +from bacpypes.apdu import ( + ReadRangeRequest, + Range, + RangeByPosition, + RangeBySequenceNumber, + RangeByTime, + ReadRangeACK, +) from bacpypes.app import BIPSimpleApplication -from bacpypes.primitivedata import ObjectIdentifier +from bacpypes.primitivedata import Date, Time, ObjectIdentifier +from bacpypes.constructeddata import Array, List +from bacpypes.basetypes import DateTime from bacpypes.local.device import LocalDeviceObject # some debugging @@ -34,17 +43,23 @@ # ReadRangeConsoleCmd # + @bacpypes_debugging class ReadRangeConsoleCmd(ConsoleCmd): - def do_readrange(self, args): - """readrange [ ]""" + """readrange [ ] + [ p ] + [ s ] + [ t