1
0
mirror of https://github.com/JoelBender/bacpypes synced 2025-10-05 22:18:16 +08:00
bacpypes/doc/source/samples/sample001.rst
2016-11-19 23:28:41 -05:00

337 lines
12 KiB
ReStructuredText

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.
Debugging and logging is brought to the application via a decorator (see later in class) and
you will need :class:`debugging.ModuleLogger`::
from bacpypes.debugging import bacpypes_debugging, ModuleLogger
All BACpypes applications gather some options from the command line and use the
:class:`consolelogging.ConfigArgumentParser` function for reading configuration
information::
from bacpypes.consolelogging import ConfigArgumentParser
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:`service.device.LocalDeviceObject`::
from bacpypes.app import BIPSimpleApplication
from bacpypes.service.device 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 function. Everything is wrapped in a
try..except..finally because many "real world" applications send startup and
shutdown notifications to other processes and it is important to include
the exception (or graceful conclusion) of the application along with the
notification::
#
# __main__
#
def main():
# code goes here...
if _debug: _log.debug("initialization")
if _debug: _log.debug(" - args: %r", args)
try:
# code goes here...
_log.debug("initialization")
# code goes here...
_log.debug("running")
# code goes here...
except Exception as e:
_log.exception("an error has occurred: %s", e)
finally:
_log.debug("finally")
if __name__ == "__main__":
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. Here is
the basic example of a INI file::
[BACpypes]
objectName: Betelgeuse
address: 192.168.1.2/24
objectIdentifier: 599
maxApduLengthAccepted: 1024
segmentationSupported: segmentedBoth
maxSegmentsAccepted: 1024
vendorIdentifier: 15
foreignPort: 0
foreignBBMD: 128.253.109.254
foreignTTL: 30
.. tip::
There is a sample INI file called **BACpypes~.ini** as part of the repository. Make
a local copy and edit it with information appropriate to your installation::
$ pwd
.../samples
$ cp ../BACpypes~.ini BACpypes.ini
$ nano BACpypes.ini
.. tip::
Windows user may want to have a look to Notepad++ as a file editor. If
using the Anaconda suite, you can use Spyder or any other text editor
you like.
The INI file must exist when you will run the code.
Filling the blanks
----------------------
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.::
# parse the command line arguments and initialize loggers
args = ConfigArgumentParser(description=__doc__).parse_args()
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__
Will output::
__main__
__main__.SampleApplication
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. Usually the debugging hooks will be added to the end of the
parameter and option list::
$ python SampleApplication.py --debug __main__
Will output::
DEBUG:__main__:initialization
DEBUG:__main__: - args: Namespace(buggers=False, color=False, debug=['__main_
_'], ini=<class 'bacpypes.consolelogging.ini'>)
DEBUG:__main__:running
DEBUG:__main__:fini
Now applications will create a :class:`service.device.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
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),
vendorName="B612",
)
.. note::
As you can see, information from the INI file is used to descrive `this_device`
The application will create a SampleApplication instance::
# make a sample application
this_application = SampleApplication(this_device, args.ini.address)
if _debug: _log.debug(" - this_application: %r", this_application)
We need to add service supported to the device using default values::
# 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
Last but not least it is time to run::
run()
SampleApplication Class
------------------------
The sample application creates a class that does almost nothing. The definition
and initialization mirrors the :class:`app.BIPSimpleApplication` and uses the
usual debugging decorator.::
#
# SampleApplication
#
@bacpypes_debugging
class SampleApplication(BIPSimpleApplication):
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 SampleApplication.py
So to see what is actually happening, run the application with debugging
enabled::
$ python SampleApplication.py --debug __main__
The output will include the initialization, running, and finally statements.::
DEBUG:__main__:initialization
DEBUG:__main__: - args: Namespace(buggers=False, color=False, debug=['__main_
_'], ini=<class 'bacpypes.consolelogging.ini'>)
DEBUG:__main__.SampleApplication:__init__ <bacpypes.service.device.LocalDeviceOb
ject object at 0x00000000026CC9B0> '192.168.1.2/24'
DEBUG:__main__: - this_application: <__main__.SampleApplication object at 0x0
00000000301FC88>
DEBUG:__main__: - services_supported: <bacpypes.basetypes.ServicesSupported
object at 0x000000000301F940>
DEBUG:__main__:running
To run with debugging on just the SampleApplication class::
$ python SampleApplication.py --debug __main__.SampleApplication
Will output::
DEBUG:__main__.SampleApplication:__init__ <bacpypes.service.device.LocalDeviceObject
object at 0x000000000231C9B0> '192.168.1.2/24'
Or to see what is happening at the UDP layer of the program, use that module name::
$ python SampleApplication.py --debug bacpypes.udp
Will output::
DEBUG:bacpypes.udp.UDPDirector:__init__ ('192.168.1.2', 47808) timeout=0
reuse=False actorClass=<class 'bacpypes.udp.UDPActor'> sid=None sapID=None
DEBUG:bacpypes.udp.UDPDirector: - getsockname: ('192.168.1.2', 47808)
Or to simplify the output to the methods of instances of the :class:`udp.UDPActor`
use the class name::
$ python SampleApplication.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 SampleApplication.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**. You can see these
messages being received and processed by the :class:`netservice.NetworkServiceElement`
buried in the stack::
$ python SampleApplication.py --debug bacpypes.netservice.NetworkServiceElement
Sending Log to a file
----------------------
The current --debug command line option takes a list of named debugging access
points and attaches a StreamHandler which sends the output to sys.stderr.
There is a way to send the debugging output to a
RotatingFileHandler by providing a file name, and optionally maxBytes and
backupCount. For example, this invocation sends the main application debugging
to standard error and the debugging output of the bacpypes.udp module to the
traffic.txt file::
$ python SampleApplication.py --debug __main__ bacpypes.udp:traffic.txt
By default the `maxBytes` is zero so there is no rotating file, but it can be
provided, for example this limits the file size to 1MB::
$ python SampleApplication.py --debug __main__ bacpypes.udp:traffic.txt:1048576
If `maxBytes` is provided, then by default the `backupCount` is 10, but it can also
be specified, so this limits the output to one hundred files::
$ python SampleApplication.py --debug __main__ bacpypes.udp:traffic.txt:1048576:100
The definition of debug::
positional arguments:
--debug [DEBUG [ DEBUG ... ]]
DEBUG ::= debugger [ : fileName [ : maxBytes [ : backupCount ]]]