Sample 1 - Simple Application

This sample application is the simplest BACpypes application that is a complete stack. Using an INI file it will configure a 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 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 app.BIPSimpleApplication and an instance of 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 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 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 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 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 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 netservice.NetworkServiceElement burried in the stack:

$ python sample001.py --debug bacpypes.netservice.NetworkServiceElement