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