diff --git a/doc/source/gettingstarted/gettingstarted001.rst b/doc/source/gettingstarted/gettingstarted001.rst index 54e19c1..455dfaf 100644 --- a/doc/source/gettingstarted/gettingstarted001.rst +++ b/doc/source/gettingstarted/gettingstarted001.rst @@ -234,7 +234,14 @@ them to be. Now start the application:: - $ python WhoIsIAm.py + $ python Tutorial/WhoIsIAm.py + +.. note:: + + The samples folder contains a Tutorial folder holding all the samples + that you will need too follow along this tutorial. + Later, the folder `HandsOnLabs` will be used as it contains the samples + that are fully explained in this document (see table of content) You will be presented with a prompt (>), and you can get help:: diff --git a/doc/source/gettingstarted/gettingstarted002.rst b/doc/source/gettingstarted/gettingstarted002.rst index 71c0eb5..5af52c9 100644 --- a/doc/source/gettingstarted/gettingstarted002.rst +++ b/doc/source/gettingstarted/gettingstarted002.rst @@ -14,7 +14,7 @@ Getting Help Whatever the command line parameters and additional options might be for an application, you can start with help:: - $ python WhoIsIAm.py --help + $ python Tutorial/WhoIsIAm.py --help usage: WhoIsIAm.py [-h] [--buggers] [--debug [DEBUG [DEBUG ...]]] [--color] [--ini INI] This application presents a 'console' prompt to the user asking for Who-Is and @@ -24,7 +24,8 @@ an application, you can start with help:: optional arguments: -h, --help show this help message and exit --buggers list the debugging logger names - --debug [DEBUG [DEBUG ...]] + --debug [DEBUG [ DEBUG ... ]] + DEBUG ::= debugger [ : fileName [ : maxBytes [ : backupCount ]]] add console log handler to each debugging logger --color use ANSI CSI color codes --ini INI device object configuration file @@ -42,7 +43,7 @@ Because BACpypes modules are deeply interconnected, dumping a complete list of all of the logger names is a long list. Start out focusing on the components of the WhoIsIAm.py application:: - $ python WhoIsIAm.py --buggers | grep __main__ + $ python Tutorial/WhoIsIAm.py --buggers | grep __main__ __main__ __main__.WhoIsIAmApplication __main__.WhoIsIAmConsoleCmd @@ -75,14 +76,14 @@ Debugging a Class Debugging all of the classes and functions can generate a lot of output, so it is useful to focus on a specific function or class:: - $ python WhoIsIAm.py --debug __main__.WhoIsIAmApplication + $ python Tutorial/WhoIsIAm.py --debug __main__.WhoIsIAmApplication DEBUG:__main__.WhoIsIAmApplication:__init__ (, '128.253.109.40/24:47808') > The same method is used to debug the activity of a BACpypes module, for example, there is a class called UDPActor in the UDP module:: - $ python WhoIsIAm.py --ini BAC0.ini --debug bacpypes.udp.UDPActor + $ python Tutorial/WhoIsIAm.py --ini BAC0.ini --debug bacpypes.udp.UDPActor > DEBUG:bacpypes.udp.UDPActor:__init__ ('128.253.109.254', 47808) DEBUG:bacpypes.udp.UDPActor:response @@ -99,6 +100,38 @@ You can debug a function just as easily. Specify as many different combinations of logger names as necessary. Note, you cannot debug a specific function within a class. +Sending Debug 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 Tutorial/WhoIsIAm.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 Tutorial/WhoIsIAm.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 Tutorial/WhoIsIAm.py --debug __main__ bacpypes.udp:traffic.txt:1048576:100 + +.. caution:: + + The traffice.txt file will be saved in the local directory (pwd) + +The definition of debug:: + + positional arguments: + --debug [DEBUG [ DEBUG ... ]] + DEBUG ::= debugger [ : fileName [ : maxBytes [ : backupCount ]]] Changing INI Files ------------------ @@ -110,11 +143,11 @@ Rather than swapping INI files, you can simply provide the INI file on the command line, overriding the default BACpypes.ini file. For example, I have an INI file for port 47808:: - $ python WhoIsIAm.py --ini BAC0.ini + $ python Tutorial/WhoIsIAm.py --ini BAC0.ini And another one for port 47809:: - $ python WhoIsIAm.py --ini BAC1.ini + $ python Tutorial/WhoIsIAm.py --ini BAC1.ini And I switch back and forth between them. diff --git a/doc/source/index.rst b/doc/source/index.rst index 1e61ec0..d941512 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -82,22 +82,24 @@ guidelines of the types of changes you might need to make. migration/migration001.rst -Samples -------- +Hands-on Lab +------------- BACpypes comes with a variety of sample applications. Some are a framework for building larger applications. Some are standalone analysis tools -that don't require a connection to a network. +that don't require a connection to a network. + +The first samples you should have a look too are located inside the +`samples/HandsOnLab` folder. Those samples are fully explained in the +documentation so you can follow along and get your head around BACpypes. + +Other less documented samples are available directly in the `samples` +folder. .. toctree:: :maxdepth: 1 - samples/sample001.rst - samples/sample002.rst - samples/sample003.rst - samples/sample004.rst - samples/sample005.rst - samples/sample014.rst + samples/sample_index.rst Glossary diff --git a/doc/source/samples/sample002.rst b/doc/source/samples/sample002.rst index 538a157..9309568 100644 --- a/doc/source/samples/sample002.rst +++ b/doc/source/samples/sample002.rst @@ -1,4 +1,3 @@ - Sample 2 - Who-Is/I-Am Counter ============================== @@ -9,6 +8,14 @@ with the regular processing. The description of this sample will be about the parts that are different from sample 1. +.. note:: + + New in 0.15 ! As you've seen reading :ref:`Capabilities`, the new API allows + mixing functionnality to application more easily. In fact, by default, + inheriting from :class:`app.BISimpleApplication` includes + :class:`service.device.WhoIsIAmServices` and + :class:`service.device.ReadWritePropertyServices` capabilities. + Counters -------- @@ -32,7 +39,9 @@ Processing Service Requests When an instance of the :class:`app.Application` receives a request it attempts to look up a function based on the message. So when a WhoIsRequest APDU is -received, there should be a do_WhoIsRequest function. +received, there should be a do_WhoIsRequest function. In fact, +:class:`services.device.WhoIsIAmServices` provides this function. For the sake +of this sample, we will override it so we can count requests. The beginning is going to be standard boiler plate function header:: @@ -57,7 +66,7 @@ processing:: # pass back to the default implementation BIPSimpleApplication.do_WhoIsRequest(self, apdu) -The do_IAmRequest function is similer:: +The do_IAmRequest function is similar:: def do_IAmRequest(self, apdu): """Given an I-Am request, cache it.""" @@ -85,10 +94,10 @@ By building the key out of elements in a useful order, it is simple enough to sort the dictionary items and print them out, and being able to unpack the key in the for loop is a nice feature of Python:: - print "----- Who Is -----" + print("----- Who Is -----") for (src, lowlim, hilim), count in sorted(who_is_counter.items()): - print "%-20s %8s %8s %4d" % (src, lowlim, hilim, count) - print + print("%-20s %8s %8s %4d" % (src, lowlim, hilim, count)) + print("") Pairing up the requests and responses can be a useful exercize, but in most cases the I-Am response from a device will be a unicast message directly back @@ -122,7 +131,7 @@ Let it run for a minute, then Press to end it. It will output its resu deviceInstanceRangeHighLimit = 59L pduData = x'' [clipped...] - ^CDEBUG:__main__:fini + DEBUG:__main__:fini ----- Who Is ----- 10001:0x0040ae007e01 1 1 1 10001:0x0040ae007e01 9830 9830 1 diff --git a/doc/source/samples/sample003.rst b/doc/source/samples/sample003.rst index f835f18..feeedd0 100644 --- a/doc/source/samples/sample003.rst +++ b/doc/source/samples/sample003.rst @@ -85,7 +85,7 @@ Running the Application Allow the application to run for a few minutes. Then end it so it will output its results.:: - ^CDEBUG:__main__:fini + DEBUG:__main__:fini ----- Who Has ----- ----- I Have ----- diff --git a/doc/source/tutorial/capability.rst b/doc/source/tutorial/capability.rst index 9dfede3..af5a943 100644 --- a/doc/source/tutorial/capability.rst +++ b/doc/source/tutorial/capability.rst @@ -8,8 +8,11 @@ separate and overlapping functionality. The original design was motivated by a component architecture where collections of components that needed to be mixed together were specified outside the application in a database. -The sample applications in this section are available in the unit test. Start -out importing the classes in the module:: +The sample applications in this section are available in tutorial folder. +Note that you can also find them in the unit test folder as they are part of the +test suites. + +Start out importing the classes in the module:: >>> from bacpypes.capability import Capability, Collector diff --git a/doc/source/tutorial/tutorial001.rst b/doc/source/tutorial/tutorial001.rst index 522a030..d542c20 100644 --- a/doc/source/tutorial/tutorial001.rst +++ b/doc/source/tutorial/tutorial001.rst @@ -5,7 +5,7 @@ Clients and Servers While exploring a library like BACpypes, take full advantage of Python being an interpreted language with an interactive prompt! The code for this tutorial -is also available in the *tutorial* subdirectory of the repository. +is also available in the *Tutorial* subdirectory of the repository. This tutorial will be using :class:`comm.Client`, :class:`comm.Server` classes, and the :func:`comm.bind` function, so start out by importing them:: diff --git a/doc/source/tutorial/tutorial006.rst b/doc/source/tutorial/tutorial006.rst index 69e6a5e..7e81561 100644 --- a/doc/source/tutorial/tutorial006.rst +++ b/doc/source/tutorial/tutorial006.rst @@ -53,7 +53,7 @@ Basic Commands All of the commands supported are listed in the :mod:`consolecmd` documentation. The simplest way to learn the commands is to try them:: - $ python SampleConsoleCmd.py + $ python Tutorial/SampleConsoleCmd.py > hi *** Unknown syntax: hi @@ -159,7 +159,7 @@ And to verify, dump the cache:: And when the sample application is run, note the new commands show up in the help list:: - $ python SampleConsoleCmd.py + $ python Tutorial/SampleConsoleCmd.py > help Documented commands (type help ): diff --git a/samples/SampleApplication.py b/samples/HandsOnLab/Sample1_SimpleApplication.py old mode 100755 new mode 100644 similarity index 100% rename from samples/SampleApplication.py rename to samples/HandsOnLab/Sample1_SimpleApplication.py diff --git a/samples/WhoIsIAmApplication.py b/samples/HandsOnLab/Sample2_WhoIsIAmApplication.py old mode 100755 new mode 100644 similarity index 100% rename from samples/WhoIsIAmApplication.py rename to samples/HandsOnLab/Sample2_WhoIsIAmApplication.py diff --git a/samples/WhoHasIHaveApplication.py b/samples/HandsOnLab/Sample3_WhoHasIHaveApplication.py old mode 100755 new mode 100644 similarity index 100% rename from samples/WhoHasIHaveApplication.py rename to samples/HandsOnLab/Sample3_WhoHasIHaveApplication.py diff --git a/samples/RandomAnalogValueObject.py b/samples/HandsOnLab/Sample4_RandomAnalogValueObject.py old mode 100755 new mode 100644 similarity index 100% rename from samples/RandomAnalogValueObject.py rename to samples/HandsOnLab/Sample4_RandomAnalogValueObject.py diff --git a/samples/Tutorial/Capabilities.py b/samples/Tutorial/Capabilities.py new file mode 100644 index 0000000..7dba110 --- /dev/null +++ b/samples/Tutorial/Capabilities.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python + +""" +The capabilty module is used to mix together classes that provide +both separate and overlapping functionality. The original design +was motivated by a component architecture where collections of +components that needed to be mixed together were specified outside +the application in a database. + +THIS FILE IS A DUPLICATE OF A UNIT TEST USED, PUT HERE FOR CONVENIENCE + +""" + +import unittest + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.capability import Capability, Collector + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +@bacpypes_debugging +class BaseCollector(Collector): + + def __init__(self): + if _debug: BaseCollector._debug("__init__") + Collector.__init__(self) + + def transform(self, value): + if _debug: BaseCollector._debug("transform %r", value) + + for fn in self.capability_functions('transform'): + print(" - fn: {}".format(fn)) + value = fn(self, value) + + return value + +@bacpypes_debugging +class PlusOne(Capability): + + def __init__(self): + if _debug: PlusOne._debug("__init__") + + def transform(self, value): + if _debug: PlusOne._debug("transform %r", value) + return value + 1 + + +@bacpypes_debugging +class TimesTen(Capability): + + def __init__(self): + if _debug: TimesTen._debug("__init__") + + def transform(self, value): + if _debug: TimesTen._debug("transform %r", value) + return value * 10 + + +@bacpypes_debugging +class MakeList(Capability): + + def __init__(self): + if _debug: MakeList._debug("__init__") + + def transform(self, value): + if _debug: MakeList._debug("transform %r", value) + return [value] + + +# +# Example classes +# + +class Example1(BaseCollector): + pass + +class Example2(BaseCollector, PlusOne): + pass + +class Example3(BaseCollector, TimesTen, PlusOne): + pass + +class Example4(BaseCollector, MakeList, TimesTen): + pass + + +@bacpypes_debugging +class TestExamples(unittest.TestCase): + + def test_example_1(self): + if _debug: TestExamples._debug("test_example_1") + + assert Example1().transform(1) == 1 + + def test_example_2(self): + if _debug: TestExamples._debug("test_example_2") + + assert Example2().transform(2) == 3 + + def test_example_3(self): + if _debug: TestExamples._debug("test_example_3") + + assert Example3().transform(3) == 31 + + def test_example_4(self): + if _debug: TestExamples._debug("test_example_4") + + assert Example4().transform(4) == [4, 4, 4, 4, 4, 4, 4, 4, 4, 4] + + def test_example_5(self): + if _debug: TestExamples._debug("test_example_5") + + obj = Example2() + obj.add_capability(MakeList) + + assert obj.transform(5) == [6] diff --git a/samples/Tutorial/ClientAndServer.py b/samples/Tutorial/ClientAndServer.py new file mode 100644 index 0000000..407ee14 --- /dev/null +++ b/samples/Tutorial/ClientAndServer.py @@ -0,0 +1,23 @@ +#!/usr/bin/env python + +""" +Simple implementation of a Client and Server +""" + +from bacpypes.comm import Client, Server, bind + + +class MyServer(Server): + def indication(self, arg): + print('working on', arg) + self.response(arg.upper()) + +class MyClient(Client): + def confirmation(self, pdu): + print('thanks for the ', pdu) + +if __name__ == '__main__': + c = MyClient() + s = MyServer() + bind(c, s) + c.request('hi') \ No newline at end of file diff --git a/samples/Tutorial/ControllerAndIOCB.py b/samples/Tutorial/ControllerAndIOCB.py new file mode 100644 index 0000000..fe8a757 --- /dev/null +++ b/samples/Tutorial/ControllerAndIOCB.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python + +""" +The IO Control Block (IOCB) is an object that holds the parameters +for some kind of operation or function and a place for the result. +The IOController processes the IOCBs it is given and returns the +IOCB back to the caller. +""" + +import bacpypes +from bacpypes.iocb import IOCB, IOController + + +class SomeController(IOController): + def process_io(self, iocb): + self.complete_io(iocb, iocb.args[0] + iocb.args[1] * iocb.kwargs['a']) + +def call_me(iocb): + """ + When a controller completes the processing of a request, + the IOCB can contain one or more functions to be called. + """ + print("call me, %r or %r" % (iocb.ioResponse, iocb.ioError)) + +if __name__ == '__main__': + iocb = IOCB(1, 2, a=3) + iocb.add_callback(call_me) + some_controller = SomeController() + some_controller.request_io(iocb) + iocb.ioComplete.wait() + + print(iocb.ioComplete) + print(iocb.ioComplete.is_set()) + print(iocb.ioState == bacpypes.iocb.COMPLETED) + print(iocb.ioState == bacpypes.iocb.ABORTED) + print(iocb.ioResponse) \ No newline at end of file diff --git a/samples/SampleConsoleCmd-A.py b/samples/Tutorial/SampleConsoleCmd-Answer.py old mode 100755 new mode 100644 similarity index 100% rename from samples/SampleConsoleCmd-A.py rename to samples/Tutorial/SampleConsoleCmd-Answer.py diff --git a/samples/SampleConsoleCmd.py b/samples/Tutorial/SampleConsoleCmd.py old mode 100755 new mode 100644 similarity index 100% rename from samples/SampleConsoleCmd.py rename to samples/Tutorial/SampleConsoleCmd.py diff --git a/samples/WhoIsIAm.py b/samples/Tutorial/WhoIsIAm.py old mode 100755 new mode 100644 similarity index 100% rename from samples/WhoIsIAm.py rename to samples/Tutorial/WhoIsIAm.py