.. BACpypes tutorial lesson 1 Sample 1 - Simple Application ============================= This sample application is the simplest BACpypes application that is a complete :term:`stack`. Using an INI file it will configure a :class:`LocalDeviceObject`, create a **SampleApplication** instance, and run, waiting for a keyboard interrupt or a TERM signal to quit. Generic Application Structure ----------------------------- There is a common pattern to all BACpypes applications such as import statements in a similar order, the same debugging initialization, and the same try...except wrapper for the __main__ outer block. All BACpypes applications gather some options from the command line and use the ConfigParser module for reading configuration information:: import sys import logging from ConfigParser import ConfigParser Immediately following the built-in module includes are those for debugging:: from bacpypes.debugging import Logging, ModuleLogger from bacpypes.consolelogging import ConsoleLogHandler For applications that communicate on the network, it needs the :func:`core.run` function:: from bacpypes.core import run Now there are usually a variety of other imports depending on what the application wants to do. This one is simple, it just needs to create a derived class of :class:`app.BIPSimpleApplication` and an instance of :class:`object.LocalDeviceObject`:: from bacpypes.app import BIPSimpleApplication from bacpypes.object import LocalDeviceObject Global variables are initialized before any other classes or functions:: # some debugging _debug = 0 _log = ModuleLogger(globals()) Now skipping down to the main block. Everything is wrapped in a try..except..finally because many "real world" applications send startup and shutdown notfications to other processes and it is important to include the exception (or graceful conclusion) of the application along with the notification:: # # __main__ # try: # code goes here... _log.debug("initialization") # code goes here... _log.debug("running") # code goes here... except Exception, e: _log.exception("an error has occurred: %s", e) finally: _log.debug("finally") Before the application specific code there is template code that lists the names of the debugging log handlers (which are affectionately called *buggers*) available to attach debug handlers. This list changes depending on what has been imported, and sometimes it's easy to get lost. The application simply quits after the list:: if ('--buggers' in sys.argv): loggers = logging.Logger.manager.loggerDict.keys() loggers.sort() for loggerName in loggers: sys.stdout.write(loggerName + '\n') sys.exit(0) You can get a quick list of the debug loggers defined in this application by looking for everything with *__main__* in the name:: $ python sample001.py --buggers | grep __main__ Now that the names of buggers are known, the *--debug* option will attach a :class:`commandlogging.ConsoleLogHandler` to each of them and consume the section of the argv list:: if ('--debug' in sys.argv): indx = sys.argv.index('--debug') i = indx + 1 while (i < len(sys.argv)) and (not sys.argv[i].startswith('--')): ConsoleLogHandler(sys.argv[i]) i += 1 del sys.argv[indx:i] Usually the debugging hooks will be added to the end of the parameter and option list:: $ python sample001.py --debug __main__ Generic Initialization ---------------------- These sample applications and other server applications are run on many machines on a BACnet intranet so INI files are used for configuration parameters. .. note:: When instances of applications are going to be run on virtual machines that are dynamically created in a cloud then most of these parameters will be gathered from the environment, like the server name and address. The INI file is usually called **BACpypes.ini** and located in the same directory as the application, but the '--ini' option is available when it's not:: # read in a configuration file config = ConfigParser() if ('--ini' in sys.argv): indx = sys.argv.index('--ini') ini_file = sys.argv[indx + 1] if not config.read(ini_file): raise RuntimeError, "configuration file %r not found" % (ini_file,) del sys.argv[indx:indx+2] elif not config.read('BACpypes.ini'): raise RuntimeError, "configuration file not found" If the sample applications are run from the subversion directory, there is a sample INI file called **BACpypes~.ini** that is part of the repository. Make a local copy *that is not part of the repository* and edit it with information appropriate to your installation:: $ pwd .../samples $ cp BACpypes~.ini BACpypes.ini $ vi BACpypes.ini $ svn status ? BACpypes.ini Subversion understands that the local copy is not part of the repository. Now applications will create a :class:`object.LocalDeviceObject` which will respond to Who-Is requests for device-address-binding procedures, and Read-Property-Requests to get more details about the device, including its object list, which will only have itself:: # make a device object thisDevice = \ LocalDeviceObject( objectName=config.get('BACpypes','objectName') , objectIdentifier=config.getint('BACpypes','objectIdentifier') , maxApduLengthAccepted=config.getint('BACpypes','maxApduLengthAccepted') , segmentationSupported=config.get('BACpypes','segmentationSupported') , vendorIdentifier=config.getint('BACpypes','vendorIdentifier') ) The application will create a SampleApplication instance:: # make a test application SampleApplication(thisDevice, config.get('BACpypes','address')) Last but not least it is time to run:: run() Sample Application ------------------ The sample application creates a class that does almost nothing. The definition and initialization mirrors the :class:`app.BIPSimpleApplication` and uses the usual debugging statements at the front of the method calls:: # # SampleApplication # class SampleApplication(BIPSimpleApplication, Logging): def __init__(self, device, address): if _debug: SampleApplication._debug("__init__ %r %r", device, address) BIPSimpleApplication.__init__(self, device, address) The following functions follow the :class:`comm.ApplicationServiceElement` design pattern. In this sample application it does not make any requests, so this override is for symmetry:: def request(self, apdu): if _debug: SampleApplication._debug("request %r", apdu) BIPSimpleApplication.request(self, apdu) This sample application will receive many requests, particularly on a busy network:: def indication(self, apdu): if _debug: SampleApplication._debug("indication %r", apdu) BIPSimpleApplication.indication(self, apdu) When the application is responding to a confirmed service request it will call its response function:: def response(self, apdu): if _debug: SampleApplication._debug("response %r", apdu) BIPSimpleApplication.response(self, apdu) Because this sample application doesn't make any requests, it will not be receiving any responses from other BACnet servers, so again this function is provided for symmetry:: def confirmation(self, apdu): if _debug: SampleApplication._debug("confirmation %r", apdu) BIPSimpleApplication.confirmation(self, apdu) Running ------- When this sample application is run without any options, nothing appears on the console because there are no statements other than debugging:: $ python sample001.py So to see what is actually happening, run the application with debugging enabled:: $ python sample001.py --debug __main__ The output will include the initialization, running, and finally statements. To run with debugging on just the SampleApplication class:: $ python sample001.py --debug __main__.SampleApplication Or to see what is happening at the UDP layer of the program, use that module name:: $ python sample001.py --debug bacpypes.udp Or to simplify the output to the methods of instances of the :class:`udp.UDPActor` use the class name:: $ python sample001.py --debug bacpypes.udp.UDPActor Then to see what BACnet packets are received and make it all the way up the stack to the application, combine the debugging:: $ python sample001.py --debug bacpypes.udp.UDPActor __main__.SampleApplication The most common broadcast messages that are *not* application layer messages are Who-Is-Router-To-Network and I-Am-Router-To-Network, and you can see these messages being received and processed by the :class:`netservice.NetworkServiceElement` burried in the stack:: $ python sample001.py --debug bacpypes.netservice.NetworkServiceElement