diff --git a/doc/source/index.rst b/doc/source/index.rst index ed7896a..d3c911b 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -19,10 +19,11 @@ And then use the setup utility to install it:: $ python setup.py install If you would like to participate in its development, please join the -`developers mailing list `_. There is also a -`Google+ `_ page that you -can add to your circles have have release notifications show up in your -stream. +`developers mailing list +`_, join the +`chat room on Gitter `_, and add the +`Google+ `_ to your +circles have have release notifications show up in your stream. Welcome aboard! diff --git a/doc/source/releasenotes.rst b/doc/source/releasenotes.rst index 465caea..2ace841 100644 --- a/doc/source/releasenotes.rst +++ b/doc/source/releasenotes.rst @@ -5,6 +5,13 @@ Release Notes This page contains release notes. +Version 0.13.6 +-------------- + +There have been lots of changes in the span between the previous published +version and this one and I haven't quite figured out how to extract the +relevent content from the git log. More to come. + Version 0.13.0 -------------- diff --git a/py25/bacpypes/apdu.py b/py25/bacpypes/apdu.py index 3f031ab..c2510d5 100755 --- a/py25/bacpypes/apdu.py +++ b/py25/bacpypes/apdu.py @@ -1302,8 +1302,8 @@ class SubscribeCOVRequest(ConfirmedRequestSequence): sequenceElements = \ [ Element('subscriberProcessIdentifier', Unsigned, 0) , Element('monitoredObjectIdentifier', ObjectIdentifier, 1) - , Element('issueConfirmedNotifications', Boolean, 2) - , Element('lifetime', Unsigned, 3) + , Element('issueConfirmedNotifications', Boolean, 2, True) + , Element('lifetime', Unsigned, 3, True) ] register_confirmed_request_type(SubscribeCOVRequest) diff --git a/py25/bacpypes/app.py b/py25/bacpypes/app.py index 8f93fbb..ef64260 100755 --- a/py25/bacpypes/app.py +++ b/py25/bacpypes/app.py @@ -151,6 +151,9 @@ class Application(ApplicationServiceElement, Logging): # keep track of the local device self.localDevice = localDevice + # bind the device object to this application + localDevice._app = self + # allow the address to be cast to the correct type if isinstance(localAddress, Address): self.localAddress = localAddress @@ -186,6 +189,9 @@ class Application(ApplicationServiceElement, Logging): # append the new object's identifier to the device's object list self.localDevice.objectList.append(object_identifier) + # let the object know which application stack it belongs to + obj._app = self + def delete_object(self, obj): """Add an object to the local collection.""" if _debug: Application._debug("delete_object %r", obj) @@ -202,6 +208,9 @@ class Application(ApplicationServiceElement, Logging): indx = self.localDevice.objectList.index(object_identifier) del self.localDevice.objectList[indx] + # make sure the object knows it's detached from an application + obj._app = None + def get_object_id(self, objid): """Return a local object or None.""" return self.objectIdentifier.get(objid, None) diff --git a/py25/bacpypes/appservice.py b/py25/bacpypes/appservice.py index fff4793..63b2ce2 100755 --- a/py25/bacpypes/appservice.py +++ b/py25/bacpypes/appservice.py @@ -1245,6 +1245,7 @@ bacpypes_debugging(StateMachineAccessPoint) class ApplicationServiceAccessPoint(ApplicationServiceElement, ServiceAccessPoint): def __init__(self, aseID=None, sapID=None): + if _debug: ApplicationServiceAccessPoint._debug("__init__ aseID=%r sapID=%r", aseID, sapID) ApplicationServiceElement.__init__(self, aseID) ServiceAccessPoint.__init__(self, sapID) diff --git a/py25/bacpypes/consolecmd.py b/py25/bacpypes/consolecmd.py index a05ee1c..5fb5be3 100755 --- a/py25/bacpypes/consolecmd.py +++ b/py25/bacpypes/consolecmd.py @@ -39,14 +39,19 @@ def console_interrupt(*args): class ConsoleCmd(cmd.Cmd, Thread, Logging): - def __init__(self, prompt="> ", allow_exec=False, stdin=None, stdout=None): + def __init__(self, prompt="> ", stdin=None, stdout=None): if _debug: ConsoleCmd._debug("__init__") cmd.Cmd.__init__(self, stdin=stdin, stdout=stdout) Thread.__init__(self, name="ConsoleCmd") - # save the prompt and exec option - self.prompt = prompt - self.allow_exec = allow_exec + # check to see if this is running interactive + self.interactive = sys.__stdin__.isatty() + + # save the prompt for interactive sessions, otherwise be quiet + if self.interactive: + self.prompt = prompt + else: + self.prompt = '' # gc counters self.type2count = {} @@ -55,10 +60,6 @@ class ConsoleCmd(cmd.Cmd, Thread, Logging): # logging handlers self.handlers = {} - # execution space for the user - self._locals = {} - self._globals = {} - # set a INT signal handler, ^C will only get sent to the # main thread and there's no way to break the readline # call initiated by this thread - sigh @@ -76,7 +77,7 @@ class ConsoleCmd(cmd.Cmd, Thread, Logging): if _debug: ConsoleCmd._debug(" - done cmdloop") # tell the main thread to stop, this thread will exit - core.stop() + core.deferred(core.stop) def onecmd(self, cmdString): if _debug: ConsoleCmd._debug('onecmd %r', cmdString) @@ -231,6 +232,7 @@ class ConsoleCmd(cmd.Cmd, Thread, Logging): def do_exit(self, args): """Exits from the console.""" if _debug: ConsoleCmd._debug("do_exit %r", args) + return -1 def do_EOF(self, args): @@ -240,6 +242,8 @@ class ConsoleCmd(cmd.Cmd, Thread, Logging): def do_shell(self, args): """Pass command to a system shell when line begins with '!'""" + if _debug: ConsoleCmd._debug("do_shell %r", args) + os.system(args) def do_help(self, args): @@ -247,7 +251,9 @@ class ConsoleCmd(cmd.Cmd, Thread, Logging): 'help' or '?' with no arguments prints a list of commands for which help is available 'help ' or '? ' gives help on """ - ## The only reason to define this method is for the help text in the doc string + if _debug: ConsoleCmd._debug("do_help %r", args) + + # the only reason to define this method is for the help text in the doc string cmd.Cmd.do_help(self, args) def preloop(self): @@ -272,10 +278,14 @@ class ConsoleCmd(cmd.Cmd, Thread, Logging): if not isinstance(err, IOError): self.stdout.write("history error: %s\n" % err) - cmd.Cmd.postloop(self) ## Clean up command completion + # clean up command completion + cmd.Cmd.postloop(self) - self.stdout.write("Exiting...\n") - core.stop() + if self.interactive: + self.stdout.write("Exiting...\n") + + # tell the core we have stopped + core.deferred(core.stop) def precmd(self, line): """ This method is called after the line has been input but before @@ -294,16 +304,4 @@ class ConsoleCmd(cmd.Cmd, Thread, Logging): """Do nothing on empty input line""" pass - def default(self, line): - """Called on an input line when the command prefix is not recognized. - If allow_exec is enabled, execute the line as Python code. - """ - if not self.allow_exec: - return cmd.Cmd.default(self, line) - - try: - exec(line) in self._locals, self._globals - except Exception, err: - self.stdout.write("%s : %s\n" % (err.__class__, err)) - bacpypes_debugging(ConsoleCmd) diff --git a/py25/bacpypes/core.py b/py25/bacpypes/core.py index 4b0a912..eb3d38b 100755 --- a/py25/bacpypes/core.py +++ b/py25/bacpypes/core.py @@ -160,6 +160,7 @@ def stop(*args): # trigger the task manager event if taskManager and taskManager.trigger: + if _debug: stop._debug(" - trigger") taskManager.trigger.set() bacpypes_debugging(stop) diff --git a/py25/bacpypes/object.py b/py25/bacpypes/object.py index a3b0cf9..1469291 100755 --- a/py25/bacpypes/object.py +++ b/py25/bacpypes/object.py @@ -183,7 +183,9 @@ class Property(Logging): self.identifier, obj, value, arrayIndex, priority, direct ) - if (not direct): + if direct: + if _debug: Property._debug(" - direct write") + else: # see if it must be provided if not self.optional and value is None: raise ValueError("%s value required" % (self.identifier,)) @@ -286,7 +288,7 @@ class WritableProperty(StandardProperty, Logging): def __init__(self, identifier, datatype, default=None, optional=False, mutable=True): if _debug: - ReadableProperty._debug("__init__ %s %s default=%r optional=%r mutable=%r", + WritableProperty._debug("__init__ %s %s default=%r optional=%r mutable=%r", identifier, datatype, default, optional, mutable ) @@ -342,6 +344,9 @@ class Object(Logging): raise PropertyError(key) initargs[key] = value + # object is detached from an application until it is added + self._app = None + # start with a clean dict of values self._values = {} @@ -388,6 +393,8 @@ class Object(Logging): # get the property prop = self._properties.get(attr) + if _debug: Object._debug(" - prop: %r", prop) + if not prop: raise PropertyError(attr) @@ -465,6 +472,19 @@ class Object(Logging): klasses = list(self.__class__.__mro__) klasses.reverse() + # print special attributes "bottom up" + previous_attrs = () + for c in klasses: + attrs = getattr(c, '_debug_contents', ()) + + # if we have seen this list already, move to the next class + if attrs is previous_attrs: + continue + + for attr in attrs: + file.write("%s%s = %s\n" % (" " * indent, attr, getattr(self, attr))) + previous_attrs = attrs + # build a list of properties "bottom up" properties = [] for c in klasses: @@ -498,6 +518,11 @@ class Object(Logging): # print out the values for prop in properties: value = prop.ReadProperty(self) + + # printing out property values that are None is tedious + if value is None: + continue + if hasattr(value, "debug_contents"): file.write("%s%s\n" % (" " * indent, prop.identifier)) value.debug_contents(indent+1, file, _ids) diff --git a/py25/bacpypes/task.py b/py25/bacpypes/task.py index 7e5d22b..6d0ffab 100755 --- a/py25/bacpypes/task.py +++ b/py25/bacpypes/task.py @@ -21,7 +21,7 @@ _task_manager = None _unscheduled_tasks = [] # only defined for linux platforms -if 'linux' in sys.platform: +if sys.platform.startswith(('linux', 'darwin')): from .event import WaitableEvent # # _Trigger @@ -39,6 +39,8 @@ if 'linux' in sys.platform: # read in the character, highlander data = self.recv(1) if _debug: _Trigger._debug(" - data: %r", data) +else: + _Trigger = None # # _Task @@ -257,7 +259,7 @@ class TaskManager(SingletonLogging): # initialize self.tasks = [] - if 'linux' in sys.platform: + if _Trigger: self.trigger = _Trigger() else: self.trigger = None diff --git a/py25/bacpypes/tcp.py b/py25/bacpypes/tcp.py index a4c73dc..0e3639b 100755 --- a/py25/bacpypes/tcp.py +++ b/py25/bacpypes/tcp.py @@ -721,11 +721,14 @@ class StreamToPacket(Client, Server): self.downstreamBuffer = {} def packetize(self, pdu, streamBuffer): - if _debug: StreamToPacket._debug("packetize %r", pdu) + if _debug: StreamToPacket._debug("packetize %r ...", pdu) + + def chop(addr): + if _debug: StreamToPacket._debug("chop %r", addr) - def Chop(addr): # get the current downstream buffer buff = streamBuffer.get(addr, '') + pdu.pduData + if _debug: StreamToPacket._debug(" - buff: %r", buff) # look for a packet while 1: @@ -733,7 +736,11 @@ class StreamToPacket(Client, Server): if packet is None: break - yield PDU(packet[0], source=pdu.pduSource, destination=pdu.pduDestination) + yield PDU(packet[0], + source=pdu.pduSource, + destination=pdu.pduDestination, + user_data=pdu.pduUserData, + ) buff = packet[1] # save what didn't get sent @@ -741,10 +748,10 @@ class StreamToPacket(Client, Server): # buffer related to the addresses if pdu.pduSource: - for pdu in Chop(pdu.pduSource): + for pdu in chop(pdu.pduSource): yield pdu if pdu.pduDestination: - for pdu in Chop(pdu.pduDestination): + for pdu in chop(pdu.pduDestination): yield pdu def indication(self, pdu): diff --git a/py27/bacpypes/apdu.py b/py27/bacpypes/apdu.py index a38461b..eb81f5e 100755 --- a/py27/bacpypes/apdu.py +++ b/py27/bacpypes/apdu.py @@ -1295,8 +1295,8 @@ class SubscribeCOVRequest(ConfirmedRequestSequence): sequenceElements = \ [ Element('subscriberProcessIdentifier', Unsigned, 0) , Element('monitoredObjectIdentifier', ObjectIdentifier, 1) - , Element('issueConfirmedNotifications', Boolean, 2) - , Element('lifetime', Unsigned, 3) + , Element('issueConfirmedNotifications', Boolean, 2, True) + , Element('lifetime', Unsigned, 3, True) ] register_confirmed_request_type(SubscribeCOVRequest) diff --git a/py27/bacpypes/app.py b/py27/bacpypes/app.py index 807e6db..7800eae 100755 --- a/py27/bacpypes/app.py +++ b/py27/bacpypes/app.py @@ -151,6 +151,9 @@ class Application(ApplicationServiceElement, Logging): # keep track of the local device self.localDevice = localDevice + # bind the device object to this application + localDevice._app = self + # allow the address to be cast to the correct type if isinstance(localAddress, Address): self.localAddress = localAddress @@ -186,6 +189,9 @@ class Application(ApplicationServiceElement, Logging): # append the new object's identifier to the device's object list self.localDevice.objectList.append(object_identifier) + # let the object know which application stack it belongs to + obj._app = self + def delete_object(self, obj): """Add an object to the local collection.""" if _debug: Application._debug("delete_object %r", obj) @@ -202,6 +208,9 @@ class Application(ApplicationServiceElement, Logging): indx = self.localDevice.objectList.index(object_identifier) del self.localDevice.objectList[indx] + # make sure the object knows it's detached from an application + obj._app = None + def get_object_id(self, objid): """Return a local object or None.""" return self.objectIdentifier.get(objid, None) diff --git a/py27/bacpypes/appservice.py b/py27/bacpypes/appservice.py index e4c35f7..12faf28 100755 --- a/py27/bacpypes/appservice.py +++ b/py27/bacpypes/appservice.py @@ -1242,6 +1242,7 @@ class StateMachineAccessPoint(DeviceInfo, Client, ServiceAccessPoint): class ApplicationServiceAccessPoint(ApplicationServiceElement, ServiceAccessPoint): def __init__(self, aseID=None, sapID=None): + if _debug: ApplicationServiceAccessPoint._debug("__init__ aseID=%r sapID=%r", aseID, sapID) ApplicationServiceElement.__init__(self, aseID) ServiceAccessPoint.__init__(self, sapID) @@ -1313,6 +1314,12 @@ class ApplicationServiceAccessPoint(ApplicationServiceElement, ServiceAccessPoin # forward the encoded packet self.request(xpdu) + # if the upper layers of the application did not assign an invoke ID, + # copy the one that was assigned on its way down the stack + if isinstance(apdu, ConfirmedRequestPDU) and apdu.apduInvokeID is None: + if _debug: ApplicationServiceAccessPoint._debug(" - pass invoke ID upstream %r", xpdu.apduInvokeID) + apdu.apduInvokeID = xpdu.apduInvokeID + def confirmation(self, apdu): if _debug: ApplicationServiceAccessPoint._debug("confirmation %r", apdu) diff --git a/py27/bacpypes/consolecmd.py b/py27/bacpypes/consolecmd.py index ae32b4f..826794b 100755 --- a/py27/bacpypes/consolecmd.py +++ b/py27/bacpypes/consolecmd.py @@ -40,14 +40,19 @@ def console_interrupt(*args): @bacpypes_debugging class ConsoleCmd(cmd.Cmd, Thread, Logging): - def __init__(self, prompt="> ", allow_exec=False, stdin=None, stdout=None): + def __init__(self, prompt="> ", stdin=None, stdout=None): if _debug: ConsoleCmd._debug("__init__") cmd.Cmd.__init__(self, stdin=stdin, stdout=stdout) Thread.__init__(self, name="ConsoleCmd") - # save the prompt and exec option - self.prompt = prompt - self.allow_exec = allow_exec + # check to see if this is running interactive + self.interactive = sys.__stdin__.isatty() + + # save the prompt for interactive sessions, otherwise be quiet + if self.interactive: + self.prompt = prompt + else: + self.prompt = '' # gc counters self.type2count = {} @@ -56,10 +61,6 @@ class ConsoleCmd(cmd.Cmd, Thread, Logging): # logging handlers self.handlers = {} - # execution space for the user - self._locals = {} - self._globals = {} - # set a INT signal handler, ^C will only get sent to the # main thread and there's no way to break the readline # call initiated by this thread - sigh @@ -77,7 +78,7 @@ class ConsoleCmd(cmd.Cmd, Thread, Logging): if _debug: ConsoleCmd._debug(" - done cmdloop") # tell the main thread to stop, this thread will exit - core.stop() + core.deferred(core.stop) def onecmd(self, cmdString): if _debug: ConsoleCmd._debug('onecmd %r', cmdString) @@ -232,15 +233,19 @@ class ConsoleCmd(cmd.Cmd, Thread, Logging): def do_exit(self, args): """Exits from the console.""" if _debug: ConsoleCmd._debug("do_exit %r", args) + return -1 def do_EOF(self, args): """Exit on system end of file character""" if _debug: ConsoleCmd._debug("do_EOF %r", args) + return self.do_exit(args) def do_shell(self, args): """Pass command to a system shell when line begins with '!'""" + if _debug: ConsoleCmd._debug("do_shell %r", args) + os.system(args) def do_help(self, args): @@ -248,7 +253,9 @@ class ConsoleCmd(cmd.Cmd, Thread, Logging): 'help' or '?' with no arguments prints a list of commands for which help is available 'help ' or '? ' gives help on """ - ## The only reason to define this method is for the help text in the doc string + if _debug: ConsoleCmd._debug("do_exit %r", args) + + # the only reason to define this method is for the help text in the doc string cmd.Cmd.do_help(self, args) def preloop(self): @@ -273,10 +280,14 @@ class ConsoleCmd(cmd.Cmd, Thread, Logging): if not isinstance(err, IOError): self.stdout.write("history error: %s\n" % err) - cmd.Cmd.postloop(self) ## Clean up command completion + # clean up command completion + cmd.Cmd.postloop(self) - self.stdout.write("Exiting...\n") - core.stop() + if self.interactive: + self.stdout.write("Exiting...\n") + + # tell the core we have stopped + core.deferred(core.stop) def precmd(self, line): """ This method is called after the line has been input but before @@ -294,15 +305,3 @@ class ConsoleCmd(cmd.Cmd, Thread, Logging): def emptyline(self): """Do nothing on empty input line""" pass - - def default(self, line): - """Called on an input line when the command prefix is not recognized. - If allow_exec is enabled, execute the line as Python code. - """ - if not self.allow_exec: - return cmd.Cmd.default(self, line) - - try: - exec(line) in self._locals, self._globals - except Exception as err: - self.stdout.write("%s : %s\n" % (err.__class__, err)) diff --git a/py27/bacpypes/core.py b/py27/bacpypes/core.py index 1cf6f60..048d9a8 100755 --- a/py27/bacpypes/core.py +++ b/py27/bacpypes/core.py @@ -159,6 +159,7 @@ def stop(*args): # trigger the task manager event if taskManager and taskManager.trigger: + if _debug: stop._debug(" - trigger") taskManager.trigger.set() # set a TERM signal handler diff --git a/py27/bacpypes/object.py b/py27/bacpypes/object.py index 5c04974..7325fa4 100755 --- a/py27/bacpypes/object.py +++ b/py27/bacpypes/object.py @@ -183,7 +183,9 @@ class Property(Logging): self.identifier, obj, value, arrayIndex, priority, direct ) - if (not direct): + if direct: + if _debug: Property._debug(" - direct write") + else: # see if it must be provided if not self.optional and value is None: raise ValueError("%s value required" % (self.identifier,)) @@ -286,7 +288,7 @@ class WritableProperty(StandardProperty, Logging): def __init__(self, identifier, datatype, default=None, optional=False, mutable=True): if _debug: - ReadableProperty._debug("__init__ %s %s default=%r optional=%r mutable=%r", + WritableProperty._debug("__init__ %s %s default=%r optional=%r mutable=%r", identifier, datatype, default, optional, mutable ) @@ -321,6 +323,8 @@ class ObjectIdentifierProperty(ReadableProperty, Logging): class Object(Logging): + _debug_contents = ('_app',) + properties = \ [ ObjectIdentifierProperty('objectIdentifier', ObjectIdentifier, optional=False) , ReadableProperty('objectName', CharacterString, optional=False) @@ -342,6 +346,9 @@ class Object(Logging): raise PropertyError(key) initargs[key] = value + # object is detached from an application until it is added + self._app = None + # start with a clean dict of values self._values = {} @@ -425,6 +432,8 @@ class Object(Logging): # get the property prop = self._properties.get(propid) + if _debug: Object._debug(" - prop: %r", prop) + if not prop: raise PropertyError(propid) @@ -436,6 +445,8 @@ class Object(Logging): # get the property prop = self._properties.get(propid) + if _debug: Object._debug(" - prop: %r", prop) + if not prop: raise PropertyError(propid) @@ -490,19 +501,40 @@ class Object(Logging): klasses = list(self.__class__.__mro__) klasses.reverse() - # build a list of properties "bottom up" - properties = [] + # print special attributes "bottom up" + previous_attrs = () for c in klasses: - properties.extend(getattr(c, 'properties', [])) + attrs = getattr(c, '_debug_contents', ()) + + # if we have seen this list already, move to the next class + if attrs is previous_attrs: + continue + + for attr in attrs: + file.write("%s%s = %s\n" % (" " * indent, attr, getattr(self, attr))) + previous_attrs = attrs + + # build a list of properties "bottom up" + property_names = [] + for c in klasses: + properties = getattr(c, 'properties', []) + for property in properties: + if property.identifier not in property_names: + property_names.append(property.identifier) # print out the values - for prop in properties: - value = prop.ReadProperty(self) - if hasattr(value, "debug_contents"): - file.write("%s%s\n" % (" " * indent, prop.identifier)) - value.debug_contents(indent+1, file, _ids) + for property_name in property_names: + property_value = self._values.get(property_name, None) + + # printing out property values that are None is tedious + if property_value is None: + continue + + if hasattr(property_value, "debug_contents"): + file.write("%s%s\n" % (" " * indent, property_name)) + property_value.debug_contents(indent+1, file, _ids) else: - file.write("%s%s = %r\n" % (" " * indent, prop.identifier, value)) + file.write("%s%s = %r\n" % (" " * indent, property_name, property_value)) # # Standard Object Types diff --git a/py27/bacpypes/task.py b/py27/bacpypes/task.py index 99e3ec1..34d5cc6 100755 --- a/py27/bacpypes/task.py +++ b/py27/bacpypes/task.py @@ -21,7 +21,7 @@ _task_manager = None _unscheduled_tasks = [] # only defined for linux platforms -if 'linux' in sys.platform: +if sys.platform.startswith(('linux', 'darwin')): from .event import WaitableEvent # # _Trigger @@ -39,6 +39,8 @@ if 'linux' in sys.platform: # read in the character, highlander data = self.recv(1) if _debug: _Trigger._debug(" - data: %r", data) +else: + _Trigger = None # # _Task @@ -253,7 +255,7 @@ class TaskManager(SingletonLogging): # initialize self.tasks = [] - if 'linux' in sys.platform: + if _Trigger: self.trigger = _Trigger() else: self.trigger = None diff --git a/py27/bacpypes/tcp.py b/py27/bacpypes/tcp.py index c5a821a..574178a 100755 --- a/py27/bacpypes/tcp.py +++ b/py27/bacpypes/tcp.py @@ -715,19 +715,27 @@ class StreamToPacket(Client, Server): self.downstreamBuffer = {} def packetize(self, pdu, streamBuffer): - if _debug: StreamToPacket._debug("packetize %r", pdu) + if _debug: StreamToPacket._debug("packetize %r ...", pdu) + + def chop(addr): + if _debug: StreamToPacket._debug("chop %r", addr) - def Chop(addr): # get the current downstream buffer - buff = streamBuffer.get(addr, '') + pdu.pduData + buff = streamBuffer.get(addr, b'') + pdu.pduData + if _debug: StreamToPacket._debug(" - buff: %r", buff) # look for a packet while 1: packet = self.packetFn(buff) + if _debug: StreamToPacket._debug(" - packet: %r", packet) if packet is None: break - yield PDU(packet[0], source=pdu.pduSource, destination=pdu.pduDestination) + yield PDU(packet[0], + source=pdu.pduSource, + destination=pdu.pduDestination, + user_data=pdu.pduUserData, + ) buff = packet[1] # save what didn't get sent @@ -735,10 +743,10 @@ class StreamToPacket(Client, Server): # buffer related to the addresses if pdu.pduSource: - for pdu in Chop(pdu.pduSource): + for pdu in chop(pdu.pduSource): yield pdu if pdu.pduDestination: - for pdu in Chop(pdu.pduDestination): + for pdu in chop(pdu.pduDestination): yield pdu def indication(self, pdu): @@ -777,8 +785,8 @@ class StreamToPacketSAP(ApplicationServiceElement, ServiceAccessPoint): if addPeer: # create empty buffers associated with the peer - self.stp.upstreamBuffer[addPeer] = '' - self.stp.downstreamBuffer[addPeer] = '' + self.stp.upstreamBuffer[addPeer] = b'' + self.stp.downstreamBuffer[addPeer] = b'' if delPeer: # delete the buffer contents associated with the peer diff --git a/py34/bacpypes/apdu.py b/py34/bacpypes/apdu.py index fc345b4..09b0391 100755 --- a/py34/bacpypes/apdu.py +++ b/py34/bacpypes/apdu.py @@ -1294,8 +1294,8 @@ class SubscribeCOVRequest(ConfirmedRequestSequence): sequenceElements = \ [ Element('subscriberProcessIdentifier', Unsigned, 0) , Element('monitoredObjectIdentifier', ObjectIdentifier, 1) - , Element('issueConfirmedNotifications', Boolean, 2) - , Element('lifetime', Unsigned, 3) + , Element('issueConfirmedNotifications', Boolean, 2, True) + , Element('lifetime', Unsigned, 3, True) ] register_confirmed_request_type(SubscribeCOVRequest) diff --git a/py34/bacpypes/app.py b/py34/bacpypes/app.py index 3d74d1c..b648bd2 100755 --- a/py34/bacpypes/app.py +++ b/py34/bacpypes/app.py @@ -153,6 +153,9 @@ class Application(ApplicationServiceElement): # keep track of the local device self.localDevice = localDevice + # bind the device object to this application + localDevice._app = self + # allow the address to be cast to the correct type if isinstance(localAddress, Address): self.localAddress = localAddress @@ -188,6 +191,9 @@ class Application(ApplicationServiceElement): # append the new object's identifier to the device's object list self.localDevice.objectList.append(object_identifier) + # let the object know which application stack it belongs to + obj._app = self + def delete_object(self, obj): """Add an object to the local collection.""" if _debug: Application._debug("delete_object %r", obj) @@ -204,6 +210,9 @@ class Application(ApplicationServiceElement): indx = self.localDevice.objectList.index(object_identifier) del self.localDevice.objectList[indx] + # make sure the object knows it's detached from an application + obj._app = None + def get_object_id(self, objid): """Return a local object or None.""" return self.objectIdentifier.get(objid, None) diff --git a/py34/bacpypes/appservice.py b/py34/bacpypes/appservice.py index bff5ffa..d266d45 100755 --- a/py34/bacpypes/appservice.py +++ b/py34/bacpypes/appservice.py @@ -1242,6 +1242,7 @@ class StateMachineAccessPoint(DeviceInfo, Client, ServiceAccessPoint): class ApplicationServiceAccessPoint(ApplicationServiceElement, ServiceAccessPoint): def __init__(self, aseID=None, sapID=None): + if _debug: ApplicationServiceAccessPoint._debug("__init__ aseID=%r sapID=%r", aseID, sapID) ApplicationServiceElement.__init__(self, aseID) ServiceAccessPoint.__init__(self, sapID) diff --git a/py34/bacpypes/consolecmd.py b/py34/bacpypes/consolecmd.py index a2b5828..f348d30 100755 --- a/py34/bacpypes/consolecmd.py +++ b/py34/bacpypes/consolecmd.py @@ -40,14 +40,19 @@ def console_interrupt(*args): @bacpypes_debugging class ConsoleCmd(cmd.Cmd, Thread, Logging): - def __init__(self, prompt="> ", allow_exec=False, stdin=None, stdout=None): + def __init__(self, prompt="> ", stdin=None, stdout=None): if _debug: ConsoleCmd._debug("__init__") cmd.Cmd.__init__(self, stdin=stdin, stdout=stdout) Thread.__init__(self, name="ConsoleCmd") - # save the prompt and exec option - self.prompt = prompt - self.allow_exec = allow_exec + # check to see if this is running interactive + self.interactive = sys.__stdin__.isatty() + + # save the prompt for interactive sessions, otherwise be quiet + if self.interactive: + self.prompt = prompt + else: + self.prompt = '' # gc counters self.type2count = {} @@ -56,10 +61,6 @@ class ConsoleCmd(cmd.Cmd, Thread, Logging): # logging handlers self.handlers = {} - # execution space for the user - self._locals = {} - self._globals = {} - # set a INT signal handler, ^C will only get sent to the # main thread and there's no way to break the readline # call initiated by this thread - sigh @@ -77,7 +78,7 @@ class ConsoleCmd(cmd.Cmd, Thread, Logging): if _debug: ConsoleCmd._debug(" - done cmdloop") # tell the main thread to stop, this thread will exit - core.stop() + core.deferred(core.stop) def onecmd(self, cmdString): if _debug: ConsoleCmd._debug('onecmd %r', cmdString) @@ -233,15 +234,19 @@ class ConsoleCmd(cmd.Cmd, Thread, Logging): def do_exit(self, args): """Exits from the console.""" if _debug: ConsoleCmd._debug("do_exit %r", args) + return -1 def do_EOF(self, args): """Exit on system end of file character""" if _debug: ConsoleCmd._debug("do_EOF %r", args) + return self.do_exit(args) def do_shell(self, args): """Pass command to a system shell when line begins with '!'""" + if _debug: ConsoleCmd._debug("do_shell %r", args) + os.system(args) def do_help(self, args): @@ -249,7 +254,9 @@ class ConsoleCmd(cmd.Cmd, Thread, Logging): 'help' or '?' with no arguments prints a list of commands for which help is available 'help ' or '? ' gives help on """ - ## The only reason to define this method is for the help text in the doc string + if _debug: ConsoleCmd._debug("do_help %r", args) + + # the only reason to define this method is for the help text in the doc string cmd.Cmd.do_help(self, args) def preloop(self): @@ -274,10 +281,14 @@ class ConsoleCmd(cmd.Cmd, Thread, Logging): if not isinstance(err, IOError): self.stdout.write("history error: %s\n" % err) - cmd.Cmd.postloop(self) ## Clean up command completion + # clean up command completion + cmd.Cmd.postloop(self) - self.stdout.write("Exiting...\n") - core.stop() + if self.interactive: + self.stdout.write("Exiting...\n") + + # tell the core we have stopped + core.deferred(core.stop) def precmd(self, line): """ This method is called after the line has been input but before @@ -295,15 +306,3 @@ class ConsoleCmd(cmd.Cmd, Thread, Logging): def emptyline(self): """Do nothing on empty input line""" pass - - def default(self, line): - """Called on an input line when the command prefix is not recognized. - If allow_exec is enabled, execute the line as Python code. - """ - if not self.allow_exec: - return cmd.Cmd.default(self, line) - - try: - exec(line) in self._locals, self._globals - except Exception as err: - self.stdout.write("%s : %s\n" % (err.__class__, err)) diff --git a/py34/bacpypes/core.py b/py34/bacpypes/core.py index 1cf6f60..048d9a8 100755 --- a/py34/bacpypes/core.py +++ b/py34/bacpypes/core.py @@ -159,6 +159,7 @@ def stop(*args): # trigger the task manager event if taskManager and taskManager.trigger: + if _debug: stop._debug(" - trigger") taskManager.trigger.set() # set a TERM signal handler diff --git a/py34/bacpypes/object.py b/py34/bacpypes/object.py index beb314f..130d68e 100755 --- a/py34/bacpypes/object.py +++ b/py34/bacpypes/object.py @@ -183,7 +183,9 @@ class Property(Logging): self.identifier, obj, value, arrayIndex, priority, direct ) - if (not direct): + if direct: + if _debug: Property._debug(" - direct write") + else: # see if it must be provided if not self.optional and value is None: raise ValueError("%s value required" % (self.identifier,)) @@ -286,7 +288,7 @@ class WritableProperty(StandardProperty, Logging): def __init__(self, identifier, datatype, default=None, optional=False, mutable=True): if _debug: - ReadableProperty._debug("__init__ %s %s default=%r optional=%r mutable=%r", + WritableProperty._debug("__init__ %s %s default=%r optional=%r mutable=%r", identifier, datatype, default, optional, mutable ) @@ -342,6 +344,9 @@ class Object(Logging): raise PropertyError(key) initargs[key] = value + # object is detached from an application until it is added + self._app = None + # start with a clean dict of values self._values = {} @@ -388,6 +393,8 @@ class Object(Logging): # get the property prop = self._properties.get(attr) + if _debug: Object._debug(" - prop: %r", prop) + if not prop: raise PropertyError(attr) @@ -465,6 +472,19 @@ class Object(Logging): klasses = list(self.__class__.__mro__) klasses.reverse() + # print special attributes "bottom up" + previous_attrs = () + for c in klasses: + attrs = getattr(c, '_debug_contents', ()) + + # if we have seen this list already, move to the next class + if attrs is previous_attrs: + continue + + for attr in attrs: + file.write("%s%s = %s\n" % (" " * indent, attr, getattr(self, attr))) + previous_attrs = attrs + # build a list of properties "bottom up" properties = [] for c in klasses: @@ -498,6 +518,11 @@ class Object(Logging): # print out the values for prop in properties: value = prop.ReadProperty(self) + + # printing out property values that are None is tedious + if value is None: + continue + if hasattr(value, "debug_contents"): file.write("%s%s\n" % (" " * indent, prop.identifier)) value.debug_contents(indent+1, file, _ids) diff --git a/py34/bacpypes/task.py b/py34/bacpypes/task.py index 99e3ec1..34d5cc6 100755 --- a/py34/bacpypes/task.py +++ b/py34/bacpypes/task.py @@ -21,7 +21,7 @@ _task_manager = None _unscheduled_tasks = [] # only defined for linux platforms -if 'linux' in sys.platform: +if sys.platform.startswith(('linux', 'darwin')): from .event import WaitableEvent # # _Trigger @@ -39,6 +39,8 @@ if 'linux' in sys.platform: # read in the character, highlander data = self.recv(1) if _debug: _Trigger._debug(" - data: %r", data) +else: + _Trigger = None # # _Task @@ -253,7 +255,7 @@ class TaskManager(SingletonLogging): # initialize self.tasks = [] - if 'linux' in sys.platform: + if _Trigger: self.trigger = _Trigger() else: self.trigger = None diff --git a/py34/bacpypes/tcp.py b/py34/bacpypes/tcp.py index e5d5495..533b15d 100755 --- a/py34/bacpypes/tcp.py +++ b/py34/bacpypes/tcp.py @@ -715,11 +715,14 @@ class StreamToPacket(Client, Server): self.downstreamBuffer = {} def packetize(self, pdu, streamBuffer): - if _debug: StreamToPacket._debug("packetize %r", pdu) + if _debug: StreamToPacket._debug("packetize %r ...", pdu) + + def chop(addr): + if _debug: StreamToPacket._debug("chop %r", addr) - def Chop(addr): # get the current downstream buffer - buff = streamBuffer.get(addr, '') + pdu.pduData + buff = streamBuffer.get(addr, b'') + pdu.pduData + if _debug: StreamToPacket._debug(" - buff: %r", buff) # look for a packet while 1: @@ -727,7 +730,11 @@ class StreamToPacket(Client, Server): if packet is None: break - yield PDU(packet[0], source=pdu.pduSource, destination=pdu.pduDestination) + yield PDU(packet[0], + source=pdu.pduSource, + destination=pdu.pduDestination, + user_data=pdu.pduUserData, + ) buff = packet[1] # save what didn't get sent @@ -735,10 +742,10 @@ class StreamToPacket(Client, Server): # buffer related to the addresses if pdu.pduSource: - for pdu in Chop(pdu.pduSource): + for pdu in chop(pdu.pduSource): yield pdu if pdu.pduDestination: - for pdu in Chop(pdu.pduDestination): + for pdu in chop(pdu.pduDestination): yield pdu def indication(self, pdu): @@ -777,8 +784,8 @@ class StreamToPacketSAP(ApplicationServiceElement, ServiceAccessPoint): if addPeer: # create empty buffers associated with the peer - self.stp.upstreamBuffer[addPeer] = '' - self.stp.downstreamBuffer[addPeer] = '' + self.stp.upstreamBuffer[addPeer] = b'' + self.stp.downstreamBuffer[addPeer] = b'' if delPeer: # delete the buffer contents associated with the peer diff --git a/samples/COVMixin.py b/samples/COVMixin.py new file mode 100755 index 0000000..99e043c --- /dev/null +++ b/samples/COVMixin.py @@ -0,0 +1,1172 @@ +#!/usr/bin/python + +""" +This sample application shows how to extend the basic functionality of a device +to support the ReadPropertyMultiple service. +""" + +from collections import defaultdict + +from bacpypes.debugging import bacpypes_debugging, DebugContents, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser +from bacpypes.consolecmd import ConsoleCmd + +from bacpypes.core import run +from bacpypes.task import OneShotTask, TaskManager +from bacpypes.pdu import Address + +from bacpypes.constructeddata import SequenceOf, Any +from bacpypes.basetypes import DeviceAddress, COVSubscription, PropertyValue, \ + Recipient, RecipientProcess, ObjectPropertyReference +from bacpypes.app import LocalDeviceObject, BIPSimpleApplication +from bacpypes.object import Property, get_object_class, register_object_type, \ + AccessDoorObject, AccessPointObject, \ + AnalogInputObject, AnalogOutputObject, AnalogValueObject, \ + LargeAnalogValueObject, IntegerValueObject, PositiveIntegerValueObject, \ + LightingOutputObject, BinaryInputObject, BinaryOutputObject, \ + BinaryValueObject, LifeSafetyPointObject, LifeSafetyZoneObject, \ + MultiStateInputObject, MultiStateOutputObject, MultiStateValueObject, \ + OctetStringValueObject, CharacterStringValueObject, TimeValueObject, \ + DateTimeValueObject, DateValueObject, TimePatternValueObject, \ + DatePatternValueObject, DateTimePatternValueObject, \ + CredentialDataInputObject, LoadControlObject, LoopObject, \ + PulseConverterObject +from bacpypes.apdu import SubscribeCOVRequest, \ + ConfirmedCOVNotificationRequest, \ + UnconfirmedCOVNotificationRequest, \ + SimpleAckPDU, Error, RejectPDU, AbortPDU + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +_generic_criteria_classes = {} +_cov_increment_criteria_classes = {} + +# test globals +test_application = None + +# +# SubscriptionList +# + +class SubscriptionList: + + def __init__(self): + if _debug: SubscriptionList._debug("__init__") + + self.cov_subscriptions = [] + + def append(self, cov): + if _debug: SubscriptionList._debug("append %r", cov) + + self.cov_subscriptions.append(cov) + + def remove(self, cov): + if _debug: SubscriptionList._debug("remove %r", cov) + + self.cov_subscriptions.remove(cov) + + def find(self, client_addr, proc_id, obj_id): + if _debug: SubscriptionList._debug("find %r %r %r", client_addr, proc_id, obj_id) + + for cov in self.cov_subscriptions: + all_equal = (cov.client_addr == client_addr) and \ + (cov.proc_id == proc_id) and \ + (cov.obj_id == obj_id) + if _debug: SubscriptionList._debug(" - cov, all_equal: %r %r", cov, all_equal) + + if all_equal: + return cov + + return None + + def __len__(self): + if _debug: SubscriptionList._debug("__len__") + + return len(self.cov_subscriptions) + + def __iter__(self): + if _debug: SubscriptionList._debug("__iter__") + + for cov in self.cov_subscriptions: + yield cov + +bacpypes_debugging(SubscriptionList) + +# +# Subscription +# + +@bacpypes_debugging +class Subscription(OneShotTask, DebugContents): + + _debug_contents = ( + 'obj_ref', + 'client_addr', + 'proc_id', + 'obj_id', + 'confirmed', + 'lifetime', + ) + + def __init__(self, obj_ref, client_addr, proc_id, obj_id, confirmed, lifetime): + if _debug: Subscription._debug("__init__ %r %r %r %r %r %r", obj_ref, client_addr, proc_id, obj_id, confirmed, lifetime) + OneShotTask.__init__(self) + + # save the reference to the related object + self.obj_ref = obj_ref + + # save the parameters + self.client_addr = client_addr + self.proc_id = proc_id + self.obj_id = obj_id + self.confirmed = confirmed + self.lifetime = lifetime + + # add ourselves to the subscription list for this object + obj_ref._cov_subscriptions.append(self) + + # add ourselves to the list of all active subscriptions + obj_ref._app.active_cov_subscriptions.append(self) + + # if lifetime is non-zero, schedule the subscription to expire + if lifetime != 0: + self.install_task(delta=self.lifetime) + + def cancel_subscription(self): + if _debug: Subscription._debug("cancel_subscription") + + # suspend the task + self.suspend_task() + + # remove ourselves from the other subscriptions for this object + self.obj_ref._cov_subscriptions.remove(self) + + # remove ourselves from the list of all active subscriptions + self.obj_ref._app.active_cov_subscriptions.remove(self) + + # break the object reference + self.obj_ref = None + + def renew_subscription(self, lifetime): + if _debug: Subscription._debug("renew_subscription") + + # suspend iff scheduled + if self.isScheduled: + self.suspend_task() + + # reschedule the task if its not infinite + if lifetime != 0: + self.install_task(delta=lifetime) + + def process_task(self): + if _debug: Subscription._debug("process_task") + + # subscription is canceled + self.cancel_subscription() + +# +# COVCriteria +# + +@bacpypes_debugging +class COVCriteria: + + _properties_tracked = () + _properties_reported = () + _monitored_property_reference = None + + def _check_criteria(self): + if _debug: COVCriteria._debug("_check_criteria") + + # assume nothing has changed + something_changed = False + + # check all the things + for property_name in self._properties_tracked: + property_changed = (self._values[property_name] != self._cov_properties[property_name]) + if property_changed: + if _debug: COVCriteria._debug(" - %s changed", property_name) + + # copy the new value for next time + self._cov_properties[property_name] = self._values[property_name] + + something_changed = True + + if not something_changed: + if _debug: COVCriteria._debug(" - nothing changed") + + # should send notifications + return something_changed + + +@bacpypes_debugging +class GenericCriteria(COVCriteria): + + _properties_tracked = ( + 'presentValue', + 'statusFlags', + ) + _properties_reported = ( + 'presentValue', + 'statusFlags', + ) + _monitored_property_reference = 'presentValue' + + +@bacpypes_debugging +class COVIncrementCriteria(COVCriteria): + + _properties_tracked = ( + 'presentValue', + 'statusFlags', + ) + _properties_reported = ( + 'presentValue', + 'statusFlags', + ) + _monitored_property_reference = 'presentValue' + + def _check_criteria(self): + if _debug: COVIncrementCriteria._debug("_check_criteria") + + # assume nothing has changed + something_changed = False + + # get the old and new values + old_present_value = self._cov_properties['presentValue'] + new_present_value = self._values['presentValue'] + cov_increment = self._values['covIncrement'] + + # check the difference in values + value_changed = (new_present_value <= (old_present_value - cov_increment)) \ + or (new_present_value >= (old_present_value + cov_increment)) + if value_changed: + if _debug: COVIncrementCriteria._debug(" - present value changed") + + # copy the new value for next time + self._cov_properties['presentValue'] = new_present_value + + something_changed = True + + # check the status flags + status_changed = (self._values['statusFlags'] != self._cov_properties['statusFlags']) + if status_changed: + if _debug: COVIncrementCriteria._debug(" - status flags changed") + + # copy the new value for next time + self._cov_properties['statusFlags'] = self._values['statusFlags'] + + something_changed = True + + if not something_changed: + if _debug: COVIncrementCriteria._debug(" - nothing changed") + + # should send notifications + return something_changed + +# +# Change of Value Mixin +# + +@bacpypes_debugging +class COVObjectMixin(object): + + _debug_contents = ( + '_cov_subscriptions', + '_cov_properties', + ) + + def __init__(self, **kwargs): + if _debug: COVObjectMixin._debug("__init__ %r", kwargs) + super(COVObjectMixin, self).__init__(**kwargs) + + # list of all active subscriptions + self._cov_subscriptions = SubscriptionList() + + # snapshot the properties tracked + self._cov_properties = {} + for property_name in self._properties_tracked: + self._cov_properties[property_name] = self._values[property_name] + + def __setattr__(self, attr, value): + if _debug: COVObjectMixin._debug("__setattr__ %r %r", attr, value) + + if attr.startswith('_') or attr[0].isupper() or (attr == 'debug_contents'): + return object.__setattr__(self, attr, value) + + # use the default implementation + super(COVObjectMixin, self).__setattr__(attr, value) + + # check for special properties + if attr in self._properties_tracked: + if _debug: COVObjectMixin._debug(" - property tracked") + + # check if it is significant + if self._check_criteria(): + if _debug: COVObjectMixin._debug(" - send notifications") + self._send_cov_notifications() + else: + if _debug: COVObjectMixin._debug(" - no notifications necessary") + else: + if _debug: COVObjectMixin._debug(" - property not tracked") + + def WriteProperty(self, propid, value, arrayIndex=None, priority=None, direct=False): + if _debug: COVObjectMixin._debug("WriteProperty %r %r arrayIndex=%r priority=%r", propid, value, arrayIndex, priority) + + # normalize the property identifier + if isinstance(propid, int): + # get the property + prop = self._properties.get(propid) + if _debug: Object._debug(" - prop: %r", prop) + + if not prop: + raise PropertyError(propid) + + # use the name from now on + propid = prop.identifier + if _debug: Object._debug(" - propid: %r", propid) + + # use the default implementation + super(COVObjectMixin, self).WriteProperty(propid, value, arrayIndex, priority, direct) + + # check for special properties + if propid in self._properties_tracked: + if _debug: COVObjectMixin._debug(" - property tracked") + + # check if it is significant + if self._check_criteria(): + if _debug: COVObjectMixin._debug(" - send notifications") + self._send_cov_notifications() + else: + if _debug: COVObjectMixin._debug(" - no notifications necessary") + else: + if _debug: COVObjectMixin._debug(" - property not tracked") + + def _send_cov_notifications(self): + if _debug: COVObjectMixin._debug("_send_cov_notifications") + + # check for subscriptions + if not len(self._cov_subscriptions): + return + + # get the current time from the task manager + current_time = TaskManager().get_time() + if _debug: COVObjectMixin._debug(" - current_time: %r", current_time) + + # create a list of values + list_of_values = [] + for property_name in self._properties_reported: + if _debug: COVObjectMixin._debug(" - property_name: %r", property_name) + + # get the class + property_datatype = self.get_datatype(property_name) + if _debug: COVObjectMixin._debug(" - property_datatype: %r", property_datatype) + + # build the value + bundle_value = property_datatype(self._values[property_name]) + if _debug: COVObjectMixin._debug(" - bundle_value: %r", bundle_value) + + # bundle it into a sequence + property_value = PropertyValue( + propertyIdentifier=property_name, + value=Any(bundle_value), + ) + + # add it to the list + list_of_values.append(property_value) + if _debug: COVObjectMixin._debug(" - list_of_values: %r", list_of_values) + + # loop through the subscriptions and send out notifications + for cov in self._cov_subscriptions: + if _debug: COVObjectMixin._debug(" - cov: %r", cov) + + # calculate time remaining + if not cov.lifetime: + time_remaining = 0 + else: + time_remaining = int(cov.taskTime - current_time) + + # make sure it is at least one second + if not time_remaining: + time_remaining = 1 + + # build a request with the correct type + if cov.confirmed: + request = ConfirmedCOVNotificationRequest() + else: + request = UnconfirmedCOVNotificationRequest() + + # fill in the parameters + request.pduDestination = cov.client_addr + request.subscriberProcessIdentifier = cov.proc_id + request.initiatingDeviceIdentifier = self._app.localDevice.objectIdentifier + request.monitoredObjectIdentifier = cov.obj_id + request.timeRemaining = time_remaining + request.listOfValues = list_of_values + if _debug: COVObjectMixin._debug(" - request: %r", request) + + # let the application send it + self._app.cov_notification(cov, request) + +# --------------------------- +# access door +# --------------------------- + +@bacpypes_debugging +class AccessDoorCriteria(COVCriteria): + + _properties_tracked = ( + 'presentValue', + 'statusFlags', + 'doorAlarmState', + ) + _properties_reported = ( + 'presentValue', + 'statusFlags', + 'doorAlarmState', + ) + +@register_object_type +class AccessDoorObjectCOV(COVObjectMixin, AccessDoorCriteria, AccessDoorObject): + pass + +# --------------------------- +# access point +# --------------------------- + +@bacpypes_debugging +class AccessPointCriteria(COVCriteria): + + _properties_tracked = ( + 'accessEventTime', + 'statusFlags', + ) + _properties_reported = ( + 'accessEvent', + 'statusFlags', + 'accessEventTag', + 'accessEventTime', + 'accessEventCredential', + 'accessEventAuthenticationFactor', + ) + _monitored_property_reference = 'accessEvent' + +@register_object_type +class AccessPointObjectCOV(COVObjectMixin, AccessPointCriteria, AccessPointObject): + pass + +# --------------------------- +# analog objects +# --------------------------- + +@register_object_type +class AnalogInputObjectCOV(COVObjectMixin, COVIncrementCriteria, AnalogInputObject): + pass + +@register_object_type +class AnalogOutputObjectCOV(COVObjectMixin, COVIncrementCriteria, AnalogOutputObject): + pass + +@register_object_type +class AnalogValueObjectCOV(COVObjectMixin, COVIncrementCriteria, AnalogValueObject): + pass + +@register_object_type +class LargeAnalogValueObjectCOV(COVObjectMixin, COVIncrementCriteria, LargeAnalogValueObject): + pass + +@register_object_type +class IntegerValueObjectCOV(COVObjectMixin, COVIncrementCriteria, IntegerValueObject): + pass + +@register_object_type +class PositiveIntegerValueObjectCOV(COVObjectMixin, COVIncrementCriteria, PositiveIntegerValueObject): + pass + +@register_object_type +class LightingOutputObjectCOV(COVObjectMixin, COVIncrementCriteria, LightingOutputObject): + pass + +# --------------------------- +# generic objects +# --------------------------- + +@register_object_type +class BinaryInputObjectCOV(COVObjectMixin, GenericCriteria, BinaryInputObject): + pass + +@register_object_type +class BinaryOutputObjectCOV(COVObjectMixin, GenericCriteria, BinaryOutputObject): + pass + +@register_object_type +class BinaryValueObjectCOV(COVObjectMixin, GenericCriteria, BinaryValueObject): + pass + +@register_object_type +class LifeSafetyPointObjectCOV(COVObjectMixin, GenericCriteria, LifeSafetyPointObject): + pass + +@register_object_type +class LifeSafetyZoneObjectCOV(COVObjectMixin, GenericCriteria, LifeSafetyZoneObject): + pass + +@register_object_type +class MultiStateInputObjectCOV(COVObjectMixin, GenericCriteria, MultiStateInputObject): + pass + +@register_object_type +class MultiStateOutputObjectCOV(COVObjectMixin, GenericCriteria, MultiStateOutputObject): + pass + +@register_object_type +class MultiStateValueObjectCOV(COVObjectMixin, GenericCriteria, MultiStateValueObject): + pass + +@register_object_type +class OctetStringValueObjectCOV(COVObjectMixin, GenericCriteria, OctetStringValueObject): + pass + +@register_object_type +class CharacterStringValueObjectCOV(COVObjectMixin, GenericCriteria, CharacterStringValueObject): + pass + +@register_object_type +class TimeValueObjectCOV(COVObjectMixin, GenericCriteria, TimeValueObject): + pass + +@register_object_type +class DateTimeValueObjectCOV(COVObjectMixin, GenericCriteria, DateTimeValueObject): + pass + +@register_object_type +class DateValueObjectCOV(COVObjectMixin, GenericCriteria, DateValueObject): + pass + +@register_object_type +class TimePatternValueObjectCOV(COVObjectMixin, GenericCriteria, TimePatternValueObject): + pass + +@register_object_type +class DatePatternValueObjectCOV(COVObjectMixin, GenericCriteria, DatePatternValueObject): + pass + +@register_object_type +class DateTimePatternValueObjectCOV(COVObjectMixin, GenericCriteria, DateTimePatternValueObject): + pass + +# --------------------------- +# credential data input +# --------------------------- + +@bacpypes_debugging +class CredentialDataInputCriteria(COVCriteria): + + _properties_tracked = ( + 'updateTime', + 'statusFlags' + ) + _properties_reported = ( + 'presentValue', + 'statusFlags', + 'updateTime', + ) + +@register_object_type +class CredentialDataInputObjectCOV(COVObjectMixin, CredentialDataInputCriteria, CredentialDataInputObject): + pass + +# --------------------------- +# load control +# --------------------------- + +@bacpypes_debugging +class LoadControlCriteria(COVCriteria): + + _properties_tracked = ( + 'presentValue', + 'statusFlags', + 'requestedShedLevel', + 'startTime', + 'shedDuration', + 'dutyWindow', + ) + _properties_reported = ( + 'presentValue', + 'statusFlags', + 'requestedShedLevel', + 'startTime', + 'shedDuration', + 'dutyWindow', + ) + +@register_object_type +class LoadControlObjectCOV(COVObjectMixin, LoadControlCriteria, LoadControlObject): + pass + +# --------------------------- +# loop +# --------------------------- + +@register_object_type +class LoopObjectCOV(COVObjectMixin, COVIncrementCriteria, LoopObject): + pass + +# --------------------------- +# pulse converter +# --------------------------- + +@bacpypes_debugging +class PulseConverterCriteria(): + + _properties_tracked = ( + 'presentValue', + 'statusFlags', + ) + _properties_reported = ( + 'presentValue', + 'statusFlags', + ) + +@register_object_type +class PulseConverterObjectCOV(COVObjectMixin, PulseConverterCriteria, PulseConverterObject): + pass + +# +# COVApplicationMixin +# + +@bacpypes_debugging +class COVApplicationMixin(object): + + def __init__(self, *args, **kwargs): + if _debug: COVApplicationMixin._debug("__init__ %r %r", args, kwargs) + super(COVApplicationMixin, self).__init__(*args, **kwargs) + + # list of active subscriptions + self.active_cov_subscriptions = [] + + # a queue of confirmed notifications by client address + self.confirmed_notifications_queue = defaultdict(list) + + def cov_notification(self, cov, request): + if _debug: COVApplicationMixin._debug("cov_notification %s %s", str(cov), str(request)) + + # if this is confirmed, keep track of the cov + if cov.confirmed: + if _debug: COVApplicationMixin._debug(" - it's confirmed") + + notification_list = self.confirmed_notifications_queue[cov.client_addr] + notification_list.append((request, cov)) + + # if this isn't the first, wait until the first one is done + if len(notification_list) > 1: + if _debug: COVApplicationMixin._debug(" - not the first") + return + else: + if _debug: COVApplicationMixin._debug(" - it's unconfirmed") + + # send it along down the stack + super(COVApplicationMixin, self).request(request) + if _debug: COVApplicationMixin._debug(" - apduInvokeID: %r", getattr(request, 'apduInvokeID')) + + def cov_error(self, cov, request, response): + if _debug: COVApplicationMixin._debug("cov_error %r %r %r", cov, request, response) + + def cov_reject(self, cov, request, response): + if _debug: COVApplicationMixin._debug("cov_reject %r %r %r", cov, request, response) + + def cov_abort(self, cov, request, response): + if _debug: COVApplicationMixin._debug("cov_abort %r %r %r", cov, request, response) + + # delete the rest of the pending requests for this client + del self.confirmed_notifications_queue[cov.client_addr][:] + if _debug: COVApplicationMixin._debug(" - other notifications deleted") + + def confirmation(self, apdu): + if _debug: COVApplicationMixin._debug("confirmation %r", apdu) + + if _debug: COVApplicationMixin._debug(" - queue keys: %r", self.confirmed_notifications_queue.keys()) + + # if this isn't from someone we care about, toss it + if apdu.pduSource not in self.confirmed_notifications_queue: + if _debug: COVApplicationMixin._debug(" - not someone we are tracking") + + # pass along to the application + super(COVApplicationMixin, self).confirmation(apdu) + return + + # refer to the notification list for this client + notification_list = self.confirmed_notifications_queue[apdu.pduSource] + if _debug: COVApplicationMixin._debug(" - notification_list: %r", notification_list) + + # peek at the front of the list + request, cov = notification_list[0] + if _debug: COVApplicationMixin._debug(" - request: %s", request) + + # line up the invoke id + if apdu.apduInvokeID == request.apduInvokeID: + if _debug: COVApplicationMixin._debug(" - request/response align") + notification_list.pop(0) + else: + if _debug: COVApplicationMixin._debug(" - request/response do not align") + + # pass along to the application + super(COVApplicationMixin, self).confirmation(apdu) + return + + if isinstance(apdu, Error): + if _debug: COVApplicationMixin._debug(" - error: %r", apdu.errorCode) + self.cov_error(cov, request, apdu) + + elif isinstance(apdu, RejectPDU): + if _debug: COVApplicationMixin._debug(" - reject: %r", apdu.apduAbortRejectReason) + self.cov_reject(cov, request, apdu) + + elif isinstance(apdu, AbortPDU): + if _debug: COVApplicationMixin._debug(" - abort: %r", apdu.apduAbortRejectReason) + self.cov_abort(cov, request, apdu) + + # if the notification list is empty, delete the reference + if not notification_list: + if _debug: COVApplicationMixin._debug(" - no other pending notifications") + del self.confirmed_notifications_queue[apdu.pduSource] + return + + # peek at the front of the list for the next request + request, cov = notification_list[0] + if _debug: COVApplicationMixin._debug(" - next notification: %r", request) + + # send it along down the stack + super(COVApplicationMixin, self).request(request) + + def do_SubscribeCOVRequest(self, apdu): + if _debug: COVApplicationMixin._debug("do_SubscribeCOVRequest %r", apdu) + + # extract the pieces + client_addr = apdu.pduSource + proc_id = apdu.subscriberProcessIdentifier + obj_id = apdu.monitoredObjectIdentifier + confirmed = apdu.issueConfirmedNotifications + lifetime = apdu.lifetime + + # request is to cancel the subscription + cancel_subscription = (confirmed is None) and (lifetime is None) + + # find the object + obj = self.get_object_id(obj_id) + if not obj: + if _debug: COVConsoleCmd._debug(" - object not found") + self.response(Error(errorClass='object', errorCode='unknownObject', context=apdu)) + return + + # can a match be found? + cov = obj._cov_subscriptions.find(client_addr, proc_id, obj_id) + if _debug: COVConsoleCmd._debug(" - cov: %r", cov) + + # if a match was found, update the subscription + if cov: + if cancel_subscription: + if _debug: COVConsoleCmd._debug(" - cancel the subscription") + cov.cancel_subscription() + else: + if _debug: COVConsoleCmd._debug(" - renew the subscription") + cov.renew_subscription(lifetime) + else: + if cancel_subscription: + if _debug: COVConsoleCmd._debug(" - cancel a subscription that doesn't exist") + else: + if _debug: COVConsoleCmd._debug(" - create a subscription") + + cov = Subscription(obj, client_addr, proc_id, obj_id, confirmed, lifetime) + if _debug: COVConsoleCmd._debug(" - cov: %r", cov) + + # success + response = SimpleAckPDU(context=apdu) + + # return the result + self.response(response) + +# +# ActiveCOVSubscriptions +# + +@bacpypes_debugging +class ActiveCOVSubscriptions(Property): + + def __init__(self, identifier): + Property.__init__( + self, identifier, SequenceOf(COVSubscription), + default=None, optional=True, mutable=False, + ) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: ActiveCOVSubscriptions._debug("ReadProperty %s arrayIndex=%r", obj, arrayIndex) + + # get the current time from the task manager + current_time = TaskManager().get_time() + if _debug: ActiveCOVSubscriptions._debug(" - current_time: %r", current_time) + + # start with an empty sequence + cov_subscriptions = SequenceOf(COVSubscription)() + + # the obj is a DeviceObject with a reference to the application + for cov in obj._app.active_cov_subscriptions: + # calculate time remaining + if not cov.lifetime: + time_remaining = 0 + else: + time_remaining = int(cov.taskTime - current_time) + + # make sure it is at least one second + if not time_remaining: + time_remaining = 1 + + recipient_process = RecipientProcess( + recipient=Recipient( + address=DeviceAddress( + networkNumber=cov.client_addr.addrNet or 0, + macAddress=cov.client_addr.addrAddr, + ), + ), + processIdentifier=cov.proc_id, + ) + + cov_subscription = COVSubscription( + recipient=recipient_process, + monitoredPropertyReference=ObjectPropertyReference( + objectIdentifier=cov.obj_id, + propertyIdentifier=cov.obj_ref._monitored_property_reference, + ), + issueConfirmedNotifications=cov.confirmed, + timeRemaining=time_remaining, + # covIncrement=???, + ) + if _debug: ActiveCOVSubscriptions._debug(" - cov_subscription: %r", cov_subscription) + + # add the list + cov_subscriptions.append(cov_subscription) + + return cov_subscriptions + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# COVDeviceObject +# + +@bacpypes_debugging +class COVDeviceMixin(object): + + properties = [ + ActiveCOVSubscriptions('activeCovSubscriptions'), + ] + +class LocalDeviceObjectCOV(COVDeviceMixin, LocalDeviceObject): + pass + +# +# SubscribeCOVApplication +# + +@bacpypes_debugging +class SubscribeCOVApplication(COVApplicationMixin, BIPSimpleApplication): + pass + +# +# COVConsoleCmd +# + +class COVConsoleCmd(ConsoleCmd): + + def do_subscribe(self, args): + """subscribe addr proc_id obj_type obj_inst [ confirmed ] [ lifetime ] + """ + args = args.split() + if _debug: COVConsoleCmd._debug("do_subscribe %r", args) + global test_application + + try: + addr, proc_id, obj_type, obj_inst = args[:4] + + client_addr = Address(addr) + if _debug: COVConsoleCmd._debug(" - client_addr: %r", client_addr) + + proc_id = int(proc_id) + if _debug: COVConsoleCmd._debug(" - proc_id: %r", proc_id) + + 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) + obj_id = (obj_type, obj_inst) + if _debug: COVConsoleCmd._debug(" - obj_id: %r", obj_id) + + obj = test_application.get_object_id(obj_id) + if not obj: + print("object not found") + return + + if len(args) >= 5: + issue_confirmed = args[4] + if issue_confirmed == '-': + issue_confirmed = None + else: + issue_confirmed = issue_confirmed.lower() == 'true' + if _debug: COVConsoleCmd._debug(" - issue_confirmed: %r", issue_confirmed) + else: + issue_confirmed = None + + if len(args) >= 6: + lifetime = args[5] + if lifetime == '-': + lifetime = None + else: + lifetime = int(lifetime) + if _debug: COVConsoleCmd._debug(" - lifetime: %r", lifetime) + else: + lifetime = None + + # can a match be found? + cov = obj._cov_subscriptions.find(client_addr, proc_id, obj_id) + if _debug: COVConsoleCmd._debug(" - cov: %r", cov) + + # build a request + request = SubscribeCOVRequest( + subscriberProcessIdentifier=proc_id, + monitoredObjectIdentifier=obj_id, + ) + + # spoof that it came from the client + request.pduSource = client_addr + + # optional parameters + if issue_confirmed is not None: + request.issueConfirmedNotifications = issue_confirmed + if lifetime is not None: + request.lifetime = lifetime + + if _debug: COVConsoleCmd._debug(" - request: %r", request) + + # give it to the application + test_application.do_SubscribeCOVRequest(request) + + except Exception as err: + COVConsoleCmd._exception("exception: %r", err) + + def do_status(self, args): + """status [ object_name ]""" + args = args.split() + if _debug: COVConsoleCmd._debug("do_status %r", args) + global test_application + + if args: + obj = test_application.get_object_name(args[0]) + if not obj: + print("no such object") + else: + print("%s %s" % (obj.objectName, obj.objectIdentifier)) + obj.debug_contents() + else: + # dump the information about all the known objects + for obj in test_application.iter_objects(): + print("%s %s" % (obj.objectName, obj.objectIdentifier)) + obj.debug_contents() + + def do_trigger(self, args): + """trigger object_name""" + args = args.split() + if _debug: COVConsoleCmd._debug("do_trigger %r", args) + global test_application + + if not args: + print("object name required") + else: + obj = test_application.get_object_name(args[0]) + if not obj: + print("no such object") + else: + obj._send_cov_notifications() + + def do_set(self, args): + """set object_name [ . ] property_name [ = ] value""" + args = args.split() + if _debug: COVConsoleCmd._debug("do_set %r", args) + global test_application + + try: + object_name = args.pop(0) + if '.' in object_name: + object_name, property_name = object_name.split('.') + else: + property_name = args.pop(0) + if _debug: COVConsoleCmd._debug(" - object_name: %r", object_name) + if _debug: COVConsoleCmd._debug(" - property_name: %r", property_name) + + obj = test_application.get_object_name(object_name) + if _debug: COVConsoleCmd._debug(" - obj: %r", obj) + if not obj: + raise RuntimeError("object not found: %r" % (object_name,)) + + datatype = obj.get_datatype(property_name) + if _debug: COVConsoleCmd._debug(" - datatype: %r", datatype) + if not datatype: + raise RuntimeError("not a property: %r" % (property_name,)) + + # toss the equals + if args[0] == '=': + args.pop(0) + + # evaluate the value + value = eval(args.pop(0)) + if _debug: COVConsoleCmd._debug(" - raw value: %r", value) + + # see if it can be built + obj_value = datatype(value) + if _debug: COVConsoleCmd._debug(" - obj_value: %r", obj_value) + + # normalize + value = obj_value.value + if _debug: COVConsoleCmd._debug(" - normalized value: %r", value) + + # change the value + setattr(obj, property_name, value) + + except IndexError: + print(COVConsoleCmd.do_set.__doc__) + except Exception as err: + print("exception: %s" % (err,)) + + def do_write(self, args): + """write object_name [ . ] property [ = ] value""" + args = args.split() + if _debug: COVConsoleCmd._debug("do_set %r", args) + global test_application + + try: + object_name = args.pop(0) + if '.' in object_name: + object_name, property_name = object_name.split('.') + else: + property_name = args.pop(0) + if _debug: COVConsoleCmd._debug(" - object_name: %r", object_name) + if _debug: COVConsoleCmd._debug(" - property_name: %r", property_name) + + obj = test_application.get_object_name(object_name) + if _debug: COVConsoleCmd._debug(" - obj: %r", obj) + if not obj: + raise RuntimeError("object not found: %r" % (object_name,)) + + datatype = obj.get_datatype(property_name) + if _debug: COVConsoleCmd._debug(" - datatype: %r", datatype) + if not datatype: + raise RuntimeError("not a property: %r" % (property_name,)) + + # toss the equals + if args[0] == '=': + args.pop(0) + + # evaluate the value + value = eval(args.pop(0)) + if _debug: COVConsoleCmd._debug(" - raw value: %r", value) + + # see if it can be built + obj_value = datatype(value) + if _debug: COVConsoleCmd._debug(" - obj_value: %r", obj_value) + + # normalize + value = obj_value.value + if _debug: COVConsoleCmd._debug(" - normalized value: %r", value) + + # pass it along + obj.WriteProperty(property_name, value) + + except IndexError: + print(COVConsoleCmd.do_write.__doc__) + except Exception as err: + print("exception: %s" % (err,)) + +bacpypes_debugging(COVConsoleCmd) + + +def main(): + global test_application + + # make a parser + parser = ConfigArgumentParser(description=__doc__) + parser.add_argument("--console", + action="store_true", + default=False, + help="create a console", + ) + + # parse the command line arguments + args = parser.parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # make a device object + test_device = LocalDeviceObjectCOV( + objectName=args.ini.objectname, + objectIdentifier=int(args.ini.objectidentifier), + maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), + segmentationSupported=args.ini.segmentationsupported, + vendorIdentifier=int(args.ini.vendoridentifier), + ) + + # make a sample application + test_application = SubscribeCOVApplication(test_device, args.ini.address) + + # make a binary value object + test_bvo = BinaryValueObjectCOV( + objectIdentifier=('binaryValue', 1), + objectName='bvo', + presentValue='inactive', + statusFlags=[0, 0, 0, 0], + ) + _log.debug(" - test_bvo: %r", test_bvo) + + # add it to the device + test_application.add_object(test_bvo) + + # make an analog value object + test_avo = AnalogValueObjectCOV( + objectIdentifier=('analogValue', 1), + objectName='avo', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + covIncrement=1.0, + ) + _log.debug(" - test_avo: %r", test_avo) + + # add it to the device + test_application.add_object(test_avo) + _log.debug(" - object list: %r", test_device.objectList) + + # get the services supported + services_supported = test_application.get_services_supported() + if _debug: _log.debug(" - services_supported: %r", services_supported) + + # let the device object know + test_device.protocolServicesSupported = services_supported.value + + # make a console + if args.console: + test_console = COVConsoleCmd() + _log.debug(" - test_console: %r", test_console) + + _log.debug("running") + + run() + + _log.debug("fini") + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/samples/ConsoleCmd.py b/samples/ConsoleCmd.py new file mode 100755 index 0000000..1e04812 --- /dev/null +++ b/samples/ConsoleCmd.py @@ -0,0 +1,67 @@ +#!/usr/bin/python + +""" +This application is a template for applications that use the ConsoleCmd class. +""" + +import sys + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ArgumentParser +from bacpypes.consolecmd import ConsoleCmd + +from bacpypes.core import run + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +this_console = None + +# +# ConsoleCmdTemplate +# + +class ConsoleCmdTemplate(ConsoleCmd): + + def do_echo(self, args): + """echo ...""" + args = args.split() + if _debug: ConsoleCmdTemplate._debug("do_echo %r", args) + + sys.stdout.write(' '.join(args) + '\n') + +bacpypes_debugging(ConsoleCmdTemplate) + + +def main(): + global this_console + + # build a parser for the command line arguments + parser = ArgumentParser(description=__doc__) + + # sample additional argument to change the prompt + parser.add_argument( + "--prompt", type=str, + default="> ", + help="change the prompt", + ) + + # parse the command line arguments + args = parser.parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # make a console + this_console = ConsoleCmdTemplate(prompt=args.prompt) + + _log.debug("running") + + run() + + +if __name__ == "__main__": + main() + diff --git a/samples/SubscribeCOV.py b/samples/SubscribeCOV.py new file mode 100755 index 0000000..bd4ae65 --- /dev/null +++ b/samples/SubscribeCOV.py @@ -0,0 +1,233 @@ +#!/usr/bin/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. +""" + +import sys + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser +from bacpypes.consolecmd import ConsoleCmd + +from bacpypes.core import run + +from bacpypes.pdu import Address +from bacpypes.app import LocalDeviceObject, BIPSimpleApplication +from bacpypes.object import get_object_class, get_datatype + +from bacpypes.apdu import SubscribeCOVRequest, SimpleAckPDU, \ + Error, RejectPDU, AbortPDU +from bacpypes.primitivedata import Unsigned +from bacpypes.constructeddata import Array +from bacpypes.basetypes import ServicesSupported + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +this_device = None +this_application = None +this_console = None + +# how the application should respond +rsvp = (True, None, None) + +# +# SubscribeCOVApplication +# + +class SubscribeCOVApplication(BIPSimpleApplication): + + def __init__(self, *args): + if _debug: SubscribeCOVApplication._debug("__init__ %r", args) + BIPSimpleApplication.__init__(self, *args) + + # keep track of requests to line up responses + self._request = None + + def request(self, apdu): + if _debug: SubscribeCOVApplication._debug("request %r", apdu) + + # save a copy of the request + self._request = apdu + + # forward it along + BIPSimpleApplication.request(self, apdu) + + def confirmation(self, apdu): + if _debug: SubscribeCOVApplication._debug("confirmation %r", apdu) + + # continue normally + super(SubscribeCOVApplication, self).confirmation(apdu) + + def indication(self, apdu): + if _debug: SubscribeCOVApplication._debug("indication %r", apdu) + + # continue normally + super(SubscribeCOVApplication, self).indication(apdu) + + def do_ConfirmedCOVNotificationRequest(self, apdu): + if _debug: SubscribeCOVApplication._debug("do_ConfirmedCOVNotificationRequest %r", apdu) + global rsvp + + if rsvp[0]: + # success + response = SimpleAckPDU(context=apdu) + if _debug: SubscribeCOVApplication._debug(" - simple_ack: %r", response) + + elif rsvp[1]: + # reject + response = RejectPDU(reason=rsvp[1], context=apdu) + if _debug: SubscribeCOVApplication._debug(" - reject: %r", response) + + elif rsvp[2]: + # abort + response = AbortPDU(reason=rsvp[2], context=apdu) + if _debug: SubscribeCOVApplication._debug(" - abort: %r", response) + + # return the result + self.response(response) + + def do_UnconfirmedCOVNotificationRequest(self, apdu): + if _debug: SubscribeCOVApplication._debug("do_UnconfirmedCOVNotificationRequest %r", apdu) + +bacpypes_debugging(SubscribeCOVApplication) + +# +# SubscribeCOVConsoleCmd +# + +class SubscribeCOVConsoleCmd(ConsoleCmd): + + def do_subscribe(self, args): + """subscribe addr proc_id obj_type obj_inst [ confirmed ] [ lifetime ] + """ + args = args.split() + if _debug: SubscribeCOVConsoleCmd._debug("do_subscribe %r", args) + + try: + addr, proc_id, obj_type, obj_inst = args[:4] + + proc_id = int(proc_id) + + 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) + + if len(args) >= 5: + issue_confirmed = args[4] + if issue_confirmed == '-': + issue_confirmed = None + else: + issue_confirmed = issue_confirmed.lower() == 'true' + if _debug: SubscribeCOVConsoleCmd._debug(" - issue_confirmed: %r", issue_confirmed) + else: + issue_confirmed = None + + if len(args) >= 6: + lifetime = args[5] + if lifetime == '-': + lifetime = None + else: + lifetime = int(lifetime) + if _debug: SubscribeCOVConsoleCmd._debug(" - lifetime: %r", lifetime) + else: + lifetime = None + + # build a request + request = SubscribeCOVRequest( + subscriberProcessIdentifier=proc_id, + monitoredObjectIdentifier=(obj_type, obj_inst), + ) + request.pduDestination = Address(addr) + + # optional parameters + if issue_confirmed is not None: + request.issueConfirmedNotifications = issue_confirmed + if lifetime is not None: + request.lifetime = lifetime + + if _debug: SubscribeCOVConsoleCmd._debug(" - request: %r", request) + + # give it to the application + this_application.request(request) + + except Exception, e: + SubscribeCOVConsoleCmd._exception("exception: %r", e) + + def do_ack(self, args): + """ack + """ + args = args.split() + if _debug: SubscribeCOVConsoleCmd._debug("do_ack %r", args) + global rsvp + + rsvp = (True, None, None) + + def do_reject(self, args): + """reject reason + """ + args = args.split() + if _debug: SubscribeCOVConsoleCmd._debug("do_subscribe %r", args) + global rsvp + + rsvp = (False, args[0], None) + + def do_abort(self, args): + """abort reason + """ + args = args.split() + if _debug: SubscribeCOVConsoleCmd._debug("do_subscribe %r", args) + global rsvp + + rsvp = (False, None, args[0]) + +bacpypes_debugging(SubscribeCOVConsoleCmd) + +# +# __main__ +# + +try: + # 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=int(args.ini.objectidentifier), + maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), + segmentationSupported=args.ini.segmentationsupported, + vendorIdentifier=int(args.ini.vendoridentifier), + ) + + # make a simple application + this_application = SubscribeCOVApplication(this_device, args.ini.address) + + # get the services supported + services_supported = this_application.get_services_supported() + if _debug: _log.debug(" - services_supported: %r", services_supported) + + # let the device object know + this_device.protocolServicesSupported = services_supported.value + + # make a console + this_console = SubscribeCOVConsoleCmd() + + _log.debug("running") + + run() + +except Exception, e: + _log.exception("an error has occurred: %s", e) +finally: + _log.debug("finally") diff --git a/samples/TCPClient.py b/samples/TCPClient.py new file mode 100644 index 0000000..4a40d75 --- /dev/null +++ b/samples/TCPClient.py @@ -0,0 +1,145 @@ +#!/usr/bin/python + +""" +This simple TCP client application connects to a server and sends the text +entered in the console. There is no conversion from incoming streams of +content into a line or any other higher-layer concept of a packet. +""" + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger + +from bacpypes.core import run, stop +from bacpypes.task import TaskManager +from bacpypes.comm import PDU, Client, Server, bind, ApplicationServiceElement + +from bacpypes.consolelogging import ArgumentParser +from bacpypes.console import ConsoleClient +from bacpypes.tcp import TCPClientDirector + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +server_address = None + +# defaults +default_server_host = '127.0.0.1' +default_server_port = 9000 + +# +# MiddleMan +# + +class MiddleMan(Client, Server): + """ + An instance of this class sits between the TCPClientDirector and the + console client. Downstream packets from a console have no concept of a + destination, so this is added to the PDUs before being sent to the + director. The source information in upstream packets is ignored by the + console client. + """ + def indication(self, pdu): + if _debug: MiddleMan._debug("indication %r", pdu) + global server_address + + # no data means EOF, stop + if not pdu.pduData: + stop() + return + + # pass it along + self.request(PDU(pdu.pduData, destination=server_address)) + + def confirmation(self, pdu): + if _debug: MiddleMan._debug("confirmation %r", pdu) + + # pass it along + self.response(pdu) + +bacpypes_debugging(MiddleMan) + +# +# MiddleManASE +# + +class MiddleManASE(ApplicationServiceElement): + + def indication(self, addPeer=None, delPeer=None): + """ + This function is called by the TCPDirector when the client connects to + or disconnects from a server. It is called with addPeer or delPeer + keyword parameters, but not both. + """ + if _debug: MiddleManASE._debug('indication addPeer=%r delPeer=%r', addPeer, delPeer) + + if addPeer: + if _debug: MiddleManASE._debug(" - add peer %s", addPeer) + + if delPeer: + if _debug: MiddleManASE._debug(" - delete peer %s", delPeer) + + # if there are no clients, quit + if not self.elementService.clients: + if _debug: MiddleManASE._debug(" - quitting") + stop() + +bacpypes_debugging(MiddleManASE) + + +def main(): + """ + Main function, called when run as an application. + """ + global server_address + + # parse the command line arguments + parser = ArgumentParser(description=__doc__) + parser.add_argument( + "host", nargs='?', + help="address of host", + default=default_server_host, + ) + parser.add_argument( + "port", nargs='?', type=int, + help="server port", + default=default_server_port, + ) + args = parser.parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # extract the server address and port + host = args.host + port = args.port + server_address = (host, port) + if _debug: _log.debug(" - server_address: %r", server_address) + + # build the stack + this_console = ConsoleClient() + if _debug: _log.debug(" - this_console: %r", this_console) + + this_middle_man = MiddleMan() + if _debug: _log.debug(" - this_middle_man: %r", this_middle_man) + + this_director = TCPClientDirector() + if _debug: _log.debug(" - this_director: %r", this_director) + + bind(this_console, this_middle_man, this_director) + bind(MiddleManASE(), this_director) + + # create a task manager for scheduled functions + task_manager = TaskManager() + if _debug: _log.debug(" - task_manager: %r", task_manager) + + # don't wait to connect + this_director.connect(server_address) + + if _debug: _log.debug("running") + + run() + + +if __name__ == "__main__": + main() diff --git a/samples/TCPServer.py b/samples/TCPServer.py new file mode 100755 index 0000000..feb4904 --- /dev/null +++ b/samples/TCPServer.py @@ -0,0 +1,115 @@ +#!/usr/bin/python + +""" +This simple TCP server application listens for one or more client connections +and echos the incoming lines back to the client. There is no conversion from +incoming streams of content into a line or any other higher-layer concept +of a packet. +""" + +import sys +import logging + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ArgumentParser + +from bacpypes.core import run +from bacpypes.comm import PDU, Client, bind, ApplicationServiceElement +from bacpypes.tcp import TCPServerDirector + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +server_address = None + +# defaults +default_server_host = '127.0.0.1' +default_server_port = 9000 + +# +# EchoMaster +# + +class EchoMaster(Client): + + def confirmation(self, pdu): + if _debug: EchoMaster._debug('confirmation %r', pdu) + + self.request(PDU(pdu.pduData, destination=pdu.pduSource)) + +bacpypes_debugging(EchoMaster) + +# +# MiddleManASE +# + +class MiddleManASE(ApplicationServiceElement): + + def indication(self, addPeer=None, delPeer=None): + """ + This function is called by the TCPDirector when the client connects to + or disconnects from a server. It is called with addPeer or delPeer + keyword parameters, but not both. + """ + if _debug: MiddleManASE._debug('indication addPeer=%r delPeer=%r', addPeer, delPeer) + + if addPeer: + if _debug: MiddleManASE._debug(" - add peer %s", addPeer) + + if delPeer: + if _debug: MiddleManASE._debug(" - delete peer %s", delPeer) + +bacpypes_debugging(MiddleManASE) + +# +# __main__ +# + +def main(): + # parse the command line arguments + parser = ArgumentParser(description=__doc__) + parser.add_argument( + "--host", nargs='?', + help="listening address of server", + default=default_server_host, + ) + parser.add_argument( + "--port", nargs='?', type=int, + help="server port", + default=default_server_port, + ) + args = parser.parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # extract the server address and port + host = args.host + if host == "any": + host = '' + port = args.port + server_address = (host, port) + if _debug: _log.debug(" - server_address: %r", server_address) + + # create a director listening to the address + this_director = TCPServerDirector(server_address) + if _debug: _log.debug(" - this_director: %r", this_director) + + # create an echo + echo_master = EchoMaster() + if _debug: _log.debug(" - echo_master: %r", echo_master) + + # bind everything together + bind(echo_master, this_director) + bind(MiddleManASE(), this_director) + + _log.debug("running") + + run() + + +if __name__ == "__main__": + main() + diff --git a/samples/UDPConsole.py b/samples/UDPConsole.py index c4099f2..1163322 100755 --- a/samples/UDPConsole.py +++ b/samples/UDPConsole.py @@ -113,23 +113,19 @@ class MiddleMan(Client, Server): return addr, msg = line_parts - try: - address = Address(str(addr)) - if _debug: MiddleMan._debug(' - address: %r', address) - except Exception as err: - sys.stderr.write("err: invalid address %r: %r\n" % (addr, err)) - return - # check for a broadcast message - if address.addrType == Address.localBroadcastAddr: + # check the address + if addr == "*": dest = local_broadcast_tuple - if _debug: MiddleMan._debug(" - requesting local broadcast: %r", dest) - elif address.addrType == Address.localStationAddr: - dest = address.addrTuple - if _debug: MiddleMan._debug(" - requesting local station: %r", dest) + elif ':' in addr: + addr, port = addr.split(':') + if addr == "*": + dest = (local_broadcast_tuple[0], int(port)) + else: + dest = (addr, int(port)) else: - sys.stderr.write("err: invalid destination address type\n") - return + dest = (addr, local_unicast_tuple[1]) + if _debug: MiddleMan._debug(' - dest: %r', dest) # send it along try: