diff --git a/BACpypes~.ini b/BACpypes~.ini index 4b16488e..6740dc4d 100644 --- a/BACpypes~.ini +++ b/BACpypes~.ini @@ -1,6 +1,6 @@ [BACpypes] objectName: Betelgeuse -address: 128.253.109.40/24 +address: 192.168.210.64/24 objectIdentifier: 599 maxApduLengthAccepted: 1024 segmentationSupported: segmentedBoth diff --git a/README.md b/README.md index e60c6250..e666f21f 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,9 @@ # BACpypes -**Spring Surprise** - -Covered in dark, velvety chocolate, when you pop it into your Python path, stainless steel bolts spring out and plunge straight through both cheeks. - BACpypes provides a BACnet application layer and network layer written in Python for daemons, scripting, and graphical interfaces. +This is the current project, not the one over on SourceForge. [![Join the chat at https://gitter.im/JoelBender/bacpypes](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/JoelBender/bacpypes?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) + +[![Documentation Status](https://readthedocs.org/projects/bacpypes/badge/?version=latest)](http://bacpypes.readthedocs.io/en/latest/?badge=latest) + \ No newline at end of file diff --git a/doc/source/gettingstarted/gettingstarted001.rst b/doc/source/gettingstarted/gettingstarted001.rst index 5538d4e8..ed449efe 100644 --- a/doc/source/gettingstarted/gettingstarted001.rst +++ b/doc/source/gettingstarted/gettingstarted001.rst @@ -4,127 +4,182 @@ Getting Started =============== Ah, so you are interested in getting started with BACnet and Python. Welcome -to BACpypes, I hope you enjoy your journey. This tutorial starts with some +to BACpypes, I hope you enjoy your journey. This tutorial starts with just enough of the basics of BACnet to get a workstation communicating with -another device, installing the library, and downloading and configuring the -samples applications. +another device. We will cover installing the library, downloading and +configuring the samples applications. Basic Assumptions ----------------- -Assume that you are a software developer and it is your job to communicate +I will assume you are a software developer and it is your job to communicate with a device from another company that uses BACnet. Your employer has -given you a test device and purchased a copy of the standard. You have -in your office... +given you a test device and purchased a copy of the BACnet standard. I will +need... -- a development workstation running some flavor of Linux complete with - the latest version of Python 2.7 and +- a development workstation running some flavor of Linux or Windows, complete with + the latest version of Python (2.7 or >3.4) and `setup tools `_. -- a small hub you can plug in your workstation and this misterious device - and not get distracted by lots of other LAN traffic. +- a small Ethernet hub into which you can plug both your workstation and your + mysterious BACnet device, so you won't be distracted by lots of other network traffic. -Before getting this test environment set up and you are still connected +- a BACnetIP/BACnet-MSTP Router if your mysterious device is an MSTP device (BACpypes is + actually BACnet/IP software) + +- if you are running on Windows, installing Python may be a challenge. Some + Python packages make your life easier by including the core Python plus + many other data processing toolkits, so have a look at Continuum Analytics + `Anaconda `_ or Enthought + `Canopy `_. + +Before getting this test environment set up and while you are still connected to the internet, install the BACpypes library:: $ sudo easy_install bacpypes -And while you are at it, get a copy of the project from SourceForge that -has the library source code, sample code, and this documentation:: +or:: + + $ sudo pip install bacpypes + +And while you are at it, get a copy of the BACpypes project from GitHub. It +contains the library source code, sample code, and this documentation. Install +the `Git `_ software from +`here `_, then make a local copy of the +repository by cloning it:: - $ svn checkout svn://svn.code.sf.net/p/bacpypes/code/trunk bacpypes + $ git clone https://github.com/JoelBender/bacpypes.git No protocol analysis workbench would be complete without an installed copy of `Wireshark `_:: $ sudo apt-get install wireshark + +or if you use Windows, `download it here `_. + +.. caution:: + + Don't forget to **turn off your firewall** before beginning to play + with BACpypes! It will prevent you from hours of researches when + your code won't work as it should! + Configuring the Workstation --------------------------- -The test device that you have is going to come with some configuration +The mystery BACnet device you have is going to come with some configuration information by default and sometimes it is easier to set up the test -environment with same set of assumtions than come up with a fresh set +environment with my set of assumptions than come up with a fresh set from scratch. *IP Address* - The device will probably come with an IP address, assume that it + The device will probably come with an IP address, let's assume that it is 192.168.0.10, subnet mask 255.255.0.0, gateway address 192.168.0.1. You are going to be joining the same network, so pick 192.168.0.11 - for the workstation address with the same subnet mask. + for your workstation address and use the same subnet mask 255.255.0.0. + + If working with MSTP devices, base your workstation address on the address + of the BACnetIP Router. + +*Network Number* + If working with a BACnetIP router and an MSTP device, you will need to know + the network number configured inside the router. Every BACnet network **must** + have a unique numeric identifier. You will often see the magical number **2000** + but you can choose anything between 1 to 0xFFFE. + +*Device Identifier* + Every BACnet device on a BACnet network **must** have a unique numeric + identifier. This number is a 22-bit unsigned non-zero value. + It is critical this identifier be unique. Most large customers will have + someone or some group responsible for maintaining device identifiers across the + site. Keep track of the device identifier for the test device. Let's + assume that this device is **1000** and you are going to pick **1001** + for your workstation. *Device Name* - Every BACnet device on a BACnet network has a unique name which + Every BACnet device on a BACnet network should also have a unique name, which is a character string. There is nothing on a BACnet network that enforces this uniqueness, but it is a real headache for integrators when it isn't followed. You will need to pick a name for your - workstation. My collegues and I use star names so the sample - congiuration files will have "Betelgeuse". + workstation. My collegues and I use star names, so in the sample + configuration files you will see the name "Betelgeuse". An actual customer's + site will use a more formal (but less fun) naming convention. -*Device Identifier* - Every BACnet device will have a unique identifier, a 22-bit - unsigned non-zero value. It is critical that this be unique for - every device and most large customers will have someone or a - group responsible for maintaining device identifiers across the - site. Keep track of the device identifier for the test device, - assume that it is **1000** and you are going to pick **1001** - for your workstation. There are a few more configuration values that you will need, but -you won't need to change the values in the sample configuration file +you won't need to change the values in the sample configuration file until you get deeper into the protocol. *Maximum APDU Length Accepted* BACnet works on lots of different types of networks, from high speed Ethernet to "slower" and "cheaper" ARCNET or MS/TP (a serial bus protocol used for a field bus defined by BACnet). - For devices to exchange messages they have to know the maximum - size message the device can handle. + For devices to exchange messages they need to know the maximum + size message the other device can handle. *Segmentation Supported* A vast majority of BACnet communications traffic fits in one - message, but thre can be times when larger messages are - convinient and more efficient. Segmentation allows larger - messages to be broken up into segemnts and spliced back together. - It is not unusual for "low power" field equipment to not + message, but there are times when larger messages are + convenient and more efficient. Segmentation allows larger + messages to be broken up into segments and spliced back together. + It is not unusual for "low power" field devices to not support segmentation. There are other configuration parameters in the INI file that are -used by other applications, just leave them alone for now. +also used by other applications, just leave them alone for now. + Updating the INI File ~~~~~~~~~~~~~~~~~~~~~ -Now that you know what these values are going to be you can -configure the BACnet part of your workstation. Change into the +Now that you know what these values are going to be, you can +configure the BACnet portion of your workstation. Change into the samples directory that you checked out earlier, make a copy of the sample configuration file, and edit it for your site:: $ cd bacpypes/samples $ cp BACpypes~.ini BACpypes.ini -The sample applications are going to look for this file, and you -can direct them to other INI files on the command line, so it is -simple to keep multiple configurations. +.. tip:: + + The sample applications are going to look for this file. + You can direct the applications to use other INI files on the command line, so it is + simple to keep multiple configurations. + + At some point you will probably running both "client" and "server" + applications on your workstation, so you will want separate + configuration files for them. Keep in mind that BACnet devices + communicate as peers, so it is not unusual for an application to + act as both a client and a server at the same time. + +A typical BACpypes.ini file contains + + [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 -At some point you will probably running both "client" and "server" -applications on your workstation, so you will want separate -configuration files for them. Keep in mind that BACnet devices -communicate as peers, so it is not unusual for an application to -act as both a client and a server at the same time. UDP Communications Issues ------------------------- -BACnet devices comunicate using UDP rather than TCP. This is so -that devices do not need to implement a full IP stack (although +BACnet devices communicate using UDP rather than TCP. This is so +devices do not need to implement a full IP stack (although many of them do becuase they support multiple protocols, including having embedded web servers). There are two types of UDP messages; *unicast* which is a message -from one specific IP address and port to another one, and *broadcast* -which is received and processed by all devices that have the port -open. BACnet uses both types of messages and your workstation +from one specific IP address (and port) to another device's IP address +(and port); and *broadcast* messages which are sent by one device +and received and processed by all other devices that are listening +on that port. BACnet uses both types of messages and your workstation will need to receive both types. The BACpypes.ini file has an *address* parameter which is an IP @@ -134,27 +189,31 @@ number of bits in the network portion, which in turn implies a subnet mask, in this case **255.255.0.0**. Unicast messages will be sent to the IP address, and broadcast messages will be sent to the broadcast address **192.168.255.255** which is the network -portion of the configuration value will all 1's in the host -portion. +portion of the address with all 1's in the host portion. In this example, +the default port 47808 (0xBAC0) is used but you could provide and different +one, **192.168.0.11:47809/16**. -To receive both unicast and broadcast addresses, BACpypes will -open two sockets, one for unicast traffic and one that only listens +To receive both unicast and broadcast addresses, BACpypes +opens two sockets, one for unicast traffic and one that only listens for broadcast messages. The operating system will typically not allow two applications to open the same socket at the same time so to run two BACnet applciations at the same time they need to be configured with different ports. -The BACnet protocol has port 47808 (hex 0xBAC0) assigned to it -by the `Internet Assigned Numbers Authority `_, and sequentially -higher numbers are used in many applications. There are some -BACnet routing and networking isseus with this, but that is for -antoher tutorial. +.. note:: + + The BACnet protocol has been assigned port 47808 (hex 0xBAC0) by + by the `Internet Assigned Numbers Authority `_, and sequentially + higher numbers are used in many applications (i.e. 47809, 47810,...). + There are some BACnet routing and networking issues related to using these higher unoffical + ports, but that is a topic for another tutorial. + Starting An Application ----------------------- The simplest BACpypes sample application is the **WhoIsIAm.py** -application. It can send out Who-Is and I-Am messages and +application. It sends out Who-Is and I-Am messages and displays the results it receives. What are these things? As mentioned before, BACnet has unique device identifiers and @@ -169,7 +228,7 @@ a decentralized DNS service, but the names are unsigned integers. The request is broadcast on the network and the client waits around to listen for I-Am messages. The source address of the I-Am response is "bound" to the device identifier -and most communications is unicast after that. +and most communications are unicast thereafter. First, start up Wireshark on your workstation and a capture session with a BACnet capture filter:: @@ -183,11 +242,18 @@ in the I-Am packet decoding you will see some of its configuration parameters that should match what you expected them to be. -Now start the application:: +Now start the simplest tutorial application:: + + $ python samples/Tutorial/WhoIsIAm.py - $ python WhoIsIAm.py +.. note:: -You will be presented with a prompt, and you can get help:: + 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:: > help @@ -195,76 +261,97 @@ You will be presented with a prompt, and you can get help:: ======================================== EOF buggers bugin bugout exit gc help iam shell whois -The details of the commands will be described in the next -section. +The details of the commands are described in the next section. + Generating An I-Am ------------------ Now that the application is configured it is nice to see some -BACnet communications traffic. Just generate an I-Am message:: +BACnet communications traffic. Generate the basic I-Am message:: > iam -You should see your configuration parameters in the I-Am -message in Wireshark, this is a "global broadcast" message, so your -test device will see it but since your test device probably -isn't looking for you, it will not respond with anything. +You should see Wireshark capture your I-Am message containing your configuration +parameters. This is a "global broadcast" message. Your test device will see +it but since your test device probably isn't looking for you, it will not +respond to the message. + Binding to the Test Device -------------------------- -Now to confirm that the workstation can receive the -messages that the test device sends out, generate a Who-Is -request. This one will be "unconstrained" which means that -every device will respond. *Do not generate these types of -unconstrained requests on a large -network because it will create a lot of traffic that can -cause conjestion.* Here is a Who-Is:: +Next we want to confirm that your workstation can receive the +messages the test device sends out. We do this by generating a +generic Who-Is request. The request will be "unconstrained", meaning +every device that hears the message will respond with their corresponding +I-Am messages. + +.. caution:: + + Generating **unconstrained** Who-Is requests on a large network will create + a LOT of traffic, which can lead to network problems caused by the resulting + flood of messages. + +To generate the Who-Is request:: > whois -You should see the request in Wireshark and the response from -the device, and then a summary line of the response on the -workstation. +You should see the Who-Is request captured in Wireshark along with the I-Am +response from your test device, and then the details of the response displayed +on the workstation console.:: + + > whois + > pduSource = + iAmDeviceIdentifier = ('device', 1000) + maxAPDULengthAccepted = 480 + segmentationSupported = segmentedBoth + vendorID = 8 -There are a few different forms of the *whois* command this -simple application allows and you can see the basic form -with the help command:: + +There are a few different forms of the *whois* command supported by this +simple application. You can see these with the help command:: > help whois whois [ ] [ ] -This is like a BNF syntax, the whois command is optionally -followed by an address, and then optionally followed by a -low limit and high limit. The most common use of the Who-Is +This is like a BNF syntax, the **whois** command is optionally +followed by a BACnet device address, and then optionally followed by a +low (address) limit and high (address) limit. The most common use of the Who-Is request is to look for a specific device given its device identifier:: > whois 1000 1000 -And if the site has a numbering scheme for groups of BACnet -devices like all those in a specific building, then it is -common to look for all of them as a group:: +If the site has a numbering scheme for groups of BACnet devices (i.e. grouped +by building), then it is common to look for all the devices in a specific +building as a group:: > whois 203000 203099 Every once in a while a contractor might install a BACnet device that hasn't been properly configured. Assuming that -it has an IP address, you can send an unconstrained request +it has an IP address, you can send an **unconstrained Who-Is** request to the specific device and hope that it responds:: > whois 192.168.0.10 + > pduSource =
+ iAmDeviceIdentifier = ('device', 1000) + maxAPDULengthAccepted = 1024 + segmentationSupported = segmentedBoth + vendorID = 15 + There are other forms of BACnet addresses used in BACpypes, but that is a subject of an other tutorial. + What's Next ----------- -The next tutorial will describe the different ways this +The next tutorial describes the different ways this application can be run, and what the commands can tell you -about how it is working. All of the "console" applications, -those that prompt for commands, use the same basic +about how it is working. All of the "console" applications +(i.e. those that prompt for commands) use the same basic commands and work the same way. diff --git a/doc/source/gettingstarted/gettingstarted002.rst b/doc/source/gettingstarted/gettingstarted002.rst index 95944e1d..5af52c90 100644 --- a/doc/source/gettingstarted/gettingstarted002.rst +++ b/doc/source/gettingstarted/gettingstarted002.rst @@ -5,8 +5,8 @@ Running BACpypes Applications All BACpypes sample applications have the same basic set of command line options so it is easy to move between applications, turn debugging on and -and using different configurations. There may be additional options and -command parameters than the ones described in this section. +and use different configurations. There may be additional options and +command parameters than just the ones described in this section. Getting Help ------------ @@ -14,17 +14,18 @@ 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 - I-Am commands which create the related APDUs, then lines up the coorresponding + I-Am commands which create the related APDUs, then lines up the corresponding I-Am for incoming traffic and prints out the contents. 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 @@ -64,10 +65,10 @@ Telling the application to debug a module is simple:: The output is the severity code of the logger (almost always DEBUG), the name of the module, class, or function, then some message about the progress of the -application. From the output above you can see that the application is -beginning its initialization, shows the value of a variable called args, -an instance of the WhoIsIAmApplication class is created with some parameters, -and then the application starts running. +application. From the output above you can see the application initializing, +setting the args variable, creating an instance of the WhoIsIAmApplication class +(with some parameters), and then declaring itself - running. + Debugging a Class ----------------- @@ -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 @@ -92,13 +93,46 @@ example, there is a class called UDPActor in the UDP module:: In this sample, an instance of a UDPActor is created and then its response function is called with an instance of a PDU as a parameter. Following the function invocation description, the debugging output continues with the -contents of the PDU. Notice that the protocol data is printed as a hex -encoded string, and only the first 20 bytes of the message. +contents of the PDU. Notice, the protocol data is printed as a hex +encoded string (and restricted to just the first 20 bytes of the message). -You can debug a function just as easily, and specify as many different -combinations of logger names as necessary. Note that you cannot debug a +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 ------------------ @@ -109,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 d3c911b2..d9415128 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -7,25 +7,38 @@ BACpypes library for building BACnet applications using Python. Installation is easy, just:: $ sudo easy_install bacpypes + or + $ sudo pip install bacpypes + -You will be installing the latest released version. You can also check out -the latest version from GitHub:: +You will be installing the latest released version from PyPI (the Python Packages Index), +located at pypi.python.org - $ git clone https://github.com/JoelBender/bacpypes.git +.. note:: -And then use the setup utility to install it:: + You can also check out the latest version from GitHub:: + + $ git clone https://github.com/JoelBender/bacpypes.git + + And then use the setup utility to install it:: + + $ cd bacpypes + $ python setup.py install - $ cd bacpypes - $ python setup.py install -If you would like to participate in its development, please join the -`developers mailing list -`_, join the -`chat room on Gitter `_, and add the -`Google+ `_ to your -circles have have release notifications show up in your stream. +.. tip:: + + If you would like to participate in its development, please join: + + - the `developers mailing list `_, + - the `chat room on Gitter `_, and + - add `Google+ `_ to your circles to have release notifications show up in your stream. + + +**Welcome aboard!** + +------ -Welcome aboard! Getting Started --------------- @@ -39,6 +52,8 @@ downloading the sample code and communicating with a test device. gettingstarted/gettingstarted001.rst gettingstarted/gettingstarted002.rst + + Tutorial -------- @@ -53,33 +68,39 @@ essential components of a BACpypes application and how the pieces fit together. tutorial/tutorial003.rst tutorial/tutorial004.rst tutorial/tutorial006.rst + tutorial/iocb.rst + tutorial/capability.rst -Samples -------- +Migration +--------- -The library has a variety of sample applications, some of them are a framework -for building larger applications, some of them are standalone analysis tools -that don't require a connection to a network. +If you are upgrading your BACpypes applications to a newer version there are +guidelines of the types of changes you might need to make. .. toctree:: :maxdepth: 1 - samples/sample001.rst - samples/sample002.rst - samples/sample003.rst - samples/sample004.rst - samples/sample005.rst - samples/sample014.rst + migration/migration001.rst -Modules -------- +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. -This documentation is intended for BACpypes developers. +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 - modules/index.rst + samples/sample_index.rst + Glossary -------- @@ -89,6 +110,7 @@ Glossary glossary.rst + Release Notes ------------- @@ -97,6 +119,21 @@ Release Notes releasenotes.rst +------ + +Modules +------- + +.. tip:: Documentation intended for BACpypes developers. + +.. toctree:: + :maxdepth: 1 + + modules/index.rst + +----- + + Indices and tables ================== diff --git a/doc/source/migration/migration001.rst b/doc/source/migration/migration001.rst new file mode 100644 index 00000000..326bb25c --- /dev/null +++ b/doc/source/migration/migration001.rst @@ -0,0 +1,100 @@ +.. BACpypes updating applications + +Version 0.14.1 to 0.15.0 +======================== + +This update contains a significant number of changes to the way the project +code is organized. This is a guide to updating applications that use BACpypes +to fit the new API. + +The guide is divided into a series of sections for each type of change. + +LocalDeviceObject +----------------- + +There is a new `service` sub-package where the functionality to support a +specific type of behavior is in a separate module. The module names within +the `service` sub-package are inspired by and very similar to the names of +Clauses 13 through 17. + +The `bacpypes.service.device` module now contains the definition of the +`LocalDeviceObject` as well as mix-in classes to support Who-Is, I-Am, Who-Has, +and I-Have services. + +If your application contained this:: + + from bacpypes.app import LocalDeviceObject, BIPSimpleApplication + +Update it to contain this:: + + from bacpypes.app import BIPSimpleApplication + from bacpypes.service.device import LocalDeviceObject + +Application Subclasses +---------------------- + +The `Application` class in the `bacpypes.app` module no longer supports +services by default, they are mixed into derived classes as needed. There +are very few applications that actually took advantage of the `AtomicReadFile` +and `AtomicWriteFile` services, so when these were moved to their own +service module `bacpypes.service.file` it seems natural to move the +implementations of the other services to other modules as well. + +Moving this code to separate modules will facilitate BACpypes applications +building additional service modules to mix into the default ones or replace +default implementations with ones more suited to their local application +requirements. + +The exception to this is the `BIPSimpleApplication`, is the most commonly used +derived class from `Application` and I anticipated that by having it include +`WhoIsIAmServices` and `ReadWritePropertyServices` allowed existing applications +to run with fewer changes. + +If your application contained this:: + + class MyApplication(Application): + ... + +And you want to keep the old behavior, replace it with this:: + + from bacpypes.service.device import WhoIsIAmServices + from bacpypes.service.object import ReadWritePropertyServices + + class MyApplication(Application, WhoIsIAmServices, ReadWritePropertyServices): + ... + +Client-only Applications +------------------------ + +The `Application` class no longer requires a value for the `localDevice` or +`localAddress` parameters. BACpypes applications like that omit these +parameters will only be able to initiate confirmed or unconfirmed services that +do not require these objects or values. They would not be able to respond to +Who-Is requests for example. + +Client-only applications are useful when it would be advantageous to avoid the +administrative overhead for configuring something as a device, such as +network analysis applications and very simple trend data gather applications. +They are also useful for BACpypes applications that run in a Docker container +or "in the cloud". + +Sample client-only applications will be forthcoming. + +Simplified Requests +------------------- + +Some of the service modules now have additional functions that make it easier +to initiate requests. For example, in the `WhoIsIAmServices` class there are +functions for initiating a Who-Is request by a simple function:: + + def who_is(self, low_limit=None, high_limit=None, address=None): + ... + +Validating the parameters, building the `WhoIsRequest` PDU and sending it +downstream is all handled by the function. + +If your application builds common requests then you can use the new +functions or continue without them. If there are common requests that you +would like to make and have built into the library your suggestions are +always welcome. + diff --git a/doc/source/modules/capability.rst b/doc/source/modules/capability.rst new file mode 100644 index 00000000..9a55c3ea --- /dev/null +++ b/doc/source/modules/capability.rst @@ -0,0 +1,81 @@ +.. BACpypes capability module + +.. module:: capability + +Capability +========== + +Something here. + +Classes +------- + +.. class:: Capability + + .. attribute:: _zIndex + + Capability functions are ordered by this attribute. + +.. class:: Collector + + .. attribute:: capabilities + + A list of Capability derived classes that are in the inheritance + graph. + + .. method:: __init__() + + At initialization time the collector searches through the inheritance + graph and builds the list of Capability derived classes and then + calls the `__init__()` method for each of them. + + .. method:: capability_functions(fn) + + :param string fn: name of a capability function + + A generator that yields all of the functions of the Capability classes + with the given name, ordered by z-index. + + .. method:: add_capability(cls) + + :param class cls: add a Capability derived class + + Add a Capability derived class to the method resolution order of the + object. This will give the object a new value for its __class__ + attribute. The `__init__()` method will also be called with the + object instance. + + This new capability will only be given to the object, no other objects + with the same type will be given the new capability. + + .. method:: _search_capability(base) + + This private method returns a flatten list of all of the Capability + derived classes, including other Collector classes that might be in + the inheritance graph using recursion. + +Functions +--------- + +.. function:: compose_capability(base, *classes) + + :param Collector base: Collector derived class + :param Capability classes: Capability derived classes + + Create a new class composed of the base collector and the provided + capability classes. + +.. function:: add_capability(base, *classes) + + :param Collector base: Collector derived class + :param Capability classes: Capability derived classes + + Add a capability derived class to a collector base. + + .. note:: + Objects that were created *before* the additional capabilities were + added will have the new capability, but the `__init__()` functions + of the classes will not be called. + + Objects created *after* the additional capabilities were added will + have the additional capabilities with the `__init__()` functions called. diff --git a/doc/source/modules/comm.rst b/doc/source/modules/comm.rst index 14f36eff..331fe5b5 100644 --- a/doc/source/modules/comm.rst +++ b/doc/source/modules/comm.rst @@ -38,9 +38,8 @@ Protocol Data Units A Protocol Data Unit (PDU) is the name for a collection of information that is passed between two entities. It is composed of Protcol Control Information -(PCI), which usually has information about addressing and other types of -processing instructions, and data. The set of classes in this module are not -specific to BACnet. +(PCI) - information about addressing, processing instructions - and data. +The set of classes in this module are not specific to BACnet. .. class:: PCI @@ -68,6 +67,11 @@ specific to BACnet. .. class:: PDUData + The PDUData class has functions for extracting information from the front + of the data octet string, or append information to the end. These are helper + functions but may not be applicable for higher layer protocols which may + be passing significantly more complex data. + .. attribute:: pduData This attribute typically holds a simple octet string, but for higher @@ -81,16 +85,20 @@ specific to BACnet. .. method:: get_data(len) - :param integer len: the number of octets to extract off the front + :param integer len: the number of octets to extract. Extract a number of octets from the front of the data. If there are not at least `len` octets this will raise a DecodingError exception. .. method:: get_short() + + Extract a short integer (two octets) from the front of the data. .. method:: get_long() + Extract a long integer (four octets) from the front of the data. + .. method:: put(ch) :param octet ch: the octet to append to the end @@ -101,12 +109,12 @@ specific to BACnet. .. method:: put_short(n) + :param short integer: two octets to append to the end + .. method:: put_long(n) - The PDUData class has functions for gathering information from the front - of the octet string, or putting information on the end. These are helper - functions but may not be applicable for higher layer protocols which may - be passing significantly more complex data. + :param long integer: four octets to append to the end + .. class:: PDU(PCI, PDUData) diff --git a/doc/source/modules/consolecmd.rst b/doc/source/modules/consolecmd.rst index 1025cd74..34e9a253 100644 --- a/doc/source/modules/consolecmd.rst +++ b/doc/source/modules/consolecmd.rst @@ -5,7 +5,11 @@ Console Command =============== -This is a long line of text. +Python has a `cmd `_ module that makes +it easy to embed a command line interpreter in an application. BACpypes +extends this interpreter with some commands to assist debugging and runs +the interpreter in a separate thread so it does not interfere with the BACpypes +:func:`core.run` functionality. Functions --------- @@ -19,44 +23,88 @@ Classes .. class:: ConsoleCmd(cmd.Cmd, Thread) - This is a long line of text. .. method:: __init__(prompt="> ", allow_exec=False) :param string prompt: prompt for commands :param boolean allow_exec: allow non-commands to be executed - This is a long line of text. .. method:: run() - This is a long line of text. + Begin execution of the application's main event loop. Place this after the + the initialization statements. .. method:: do_something(args) :param args: commands - This is a long line of text. + Template of a function implementing a console command. + Commands -------- -.. option:: gc +.. option:: help + + List an application's console commands:: - This is a long line of text. + > help + Documented commands (type help ): + ======================================== + EOF buggers bugin bugout exit gc help nothing shell + +.. option:: gc + Print out garbage collection information:: + + > gc + Module Type Count dCount dRef + bacpypes.object OptionalProperty 787 0 0 + bacpypes.constructeddata Element 651 0 0 + bacpypes.object ReadableProperty 362 0 0 + bacpypes.object WritableProperty 44 0 0 + __future__ _Feature 7 0 0 + Queue Queue 2 0 0 + bacpypes.pdu Address 2 0 0 + bacpypes.udp UDPActor 2 1 4 + bacpypes.bvllservice UDPMultiplexer 1 0 0 + bacpypes.app DeviceInfoCache 1 0 0 + + Module Type Count dCount dRef + bacpypes.udp UDPActor 2 1 4 + .. option:: bugin - This is a long line of text. + Attach a debugger.:: + + > bugin bacpypes.task.OneShotTask + handler to bacpypes.task.OneShotTask added .. option:: bugout - This is a long line of text. + Detach a debugger.:: -.. option:: buggers + > bugout bacpypes.task.OneShotTask + handler to bacpypes.task.OneShotTask removed - This is a long line of text. +.. option:: buggers + Get a list of the available buggers.:: + + > buggers + no handlers + __main__ + bacpypes + bacpypes.apdu + bacpypes.apdu.APCI + ... + bacpypes.vlan.Network + bacpypes.vlan.Node + .. option:: exit - This is a long line of text. + Exit a BACpypes Console application.:: + + > exit + Exiting... diff --git a/doc/source/modules/core.rst b/doc/source/modules/core.rst index a2741205..7487ecb6 100644 --- a/doc/source/modules/core.rst +++ b/doc/source/modules/core.rst @@ -78,5 +78,4 @@ Functions time so that it does not starve child threads of processing time. When sleeping is enabled, and it only needs to be enabled for multithreaded - applications, it will put a damper on the thruput of the application. - + applications, it will put a damper on the throughput of the application. diff --git a/doc/source/modules/errors.rst b/doc/source/modules/errors.rst index 55d8dbd9..f453350b 100644 --- a/doc/source/modules/errors.rst +++ b/doc/source/modules/errors.rst @@ -5,9 +5,9 @@ Errors ====== -This module defines exception class for errors that it detects in the -configuration of the stack or in encoding and decoding PDUs. All of these -exceptions are derived from ValueError from the built-in exceptions module. +This module defines the exception class for errors it detects in the +configuration of the stack or in encoding or decoding PDUs. All of these +exceptions are derived from ValueError (in Python's built-in exceptions module). Classes ------- @@ -30,6 +30,6 @@ Classes This error is raised while PDU data is being decoded, which typically means some unstructured data like an octet stream is being turned into structured - data. There may be values in the pdu being decoded that are not + data. There may be values in the PDU being decoded that are not appropriate, or not enough data such as a truncated packet. diff --git a/doc/source/modules/index.rst b/doc/source/modules/index.rst index 2550ced7..cb482fe1 100644 --- a/doc/source/modules/index.rst +++ b/doc/source/modules/index.rst @@ -65,6 +65,14 @@ Application Layer app.rst appservice.rst +Services +-------- + +.. toctree:: + :maxdepth: 2 + + service/index.rst + Analysis -------- @@ -79,5 +87,6 @@ Other .. toctree:: :maxdepth: 2 + capability.rst commandlogging.rst - + iocb.rst diff --git a/doc/source/modules/iocb.rst b/doc/source/modules/iocb.rst new file mode 100644 index 00000000..b709ec62 --- /dev/null +++ b/doc/source/modules/iocb.rst @@ -0,0 +1,404 @@ +.. BACpypes IO control block module + +.. module:: iocb + +IO Control Block +================ + +The IO Control Block (IOCB) is a data structure that is used to store parameters +for some kind of processing and then used to retrieve the results of that +processing at a later time. An IO Controller (IOController) is the executor +of that processing. + +They are modeled after the VAX/VMS IO subsystem API in which a single function +could take a wide variety of combinations of parameters and the application +did not necessarily wait for the operation to complete, but could be notified +when it was by an event flag or semaphore. It could also provide a callback +function to be called when processing was complete. + +For example, given a simple function call:: + + result = some_function(arg1, arg2, kwarg1=1) + +The IOCB would contain the arguments and keyword arguments, the some_function() +would be the controller, and the result would alo be stored in the IOCB when +the function is complete. + +If the IOController encountered an error during processing, some value specifying +the error is also stored in the IOCB. + +Classes +------- + +There are two fundamental classes in this module, the :class:`IOCB` for bundling +request parameters together and processing the result, and :class:`IOController` +for executing requests. + +The :class:`IOQueue` is an object that manages a queue of IOCB requests when +some functionality needs to be processed one at a time, and an :class:`IOQController` +which has the same signature as an IOController but takes advantage of a queue. + +The :class:`IOGroup` is used to bundle a collection of requests together that +may be processed by separate controllers at different times but has `wait()` +and `add_callback()` functions and can be otherwise treated as an IOCB. + +.. class:: IOCB + + The IOCB contains a unique identifier, references to the arguments and + keyword arguments used when it was constructed, and placeholders for + processing results or errors. + + .. attribute:: ioID + + Every IOCB has a unique identifier that persists for the lifetime of + the block. Similar to the Invoke ID for confirmed services, it can be used + to synchronize communications and related functions. + + The default identifier value is a thread safe monotonically increasing + value. + + .. attribute:: args, kwargs + + These are copies of the arguments and keyword arguments passed during the + construction of the IOCB. + + .. attribute:: ioState + + The ioState of an IOCB is the state of processing for the block. + + * *idle* - an IOCB is idle when it is first constructed and before it has been given to a controller. + * *pending* - the IOCB has been given to a controller but the processing of the request has not started. + * *active* - the IOCB is being processed by the controller. + * *completed* - the processing of the IOCB has completed and the positive results have been stored in `ioResponse`. + * *aborted* - the processing of the IOCB has encountered an error of some kind and the error condition has been stored in `ioError`. + + .. attribute:: ioResponse + + The result that some controller is providing to the application that + created the IOCB. + + .. attribute:: ioError + + The error condition that the controller is providing when the processing + resulted in an error. + + .. method:: __init__(*args, **kwargs) + + :param args: arbitrary arguments + :param kwargs: arbitrary keyword arguments + + Create an IOCB and store the arguments and keyword arguments in it. The + IOCB will be given a unique identifier and start in the *idle* state. + + .. method:: complete(msg) + + :param msg: positive result of request + + .. method:: abort(msg) + + :param msg: negative results of request + + .. method:: trigger() + + This method is called by complete() or abort() after the positive or + negative result has been stored in the IOCB. + + .. method:: wait(*args) + + :param args: arbitrary arguments + + Block until the IO operation is complete and the positive or negative + result has been placed in the ICOB. The arguments are passed to the + `wait()` function of the ioComplete event. + + .. method:: add_callback(fn, *args, **kwargs) + + :param fn: the function to call when the IOCB is triggered + :param args: additional arguments passed to the function + :param kwargs: additional keyword arguments passed to the function + + Add the function `fn` to a list of functions to call when the IOCB is + triggered because it is complete or aborted. When the function is + called the first parameter will be the IOCB that was triggered. + + An IOCB can have any number of callback functions added to it and they + will be called in the order they were added to the IOCB. + + If the IOCB is has already been triggered then the callback function + will be called immediately. Callback functions are typically added + to an IOCB before it is given to a controller. + + .. method:: set_timeout(delay, err=TimeoutError) + + :param seconds delay: the time limit for processing the IOCB + :param err: the error to use when the IOCB is aborted + + Set a time limit on the amount of time an IOCB can take to be completed, + and if the time is exceeded then the IOCB is aborted. + +.. class:: IOController + + An IOController is an API for processing an IOCB. It has one method + `process_io()` provided by a derived class which will be called for each IOCB + that is requested of it. It calls one of its `complete_io()` or `abort_io()` + functions as necessary to satisfy the request. + + This class does not restrict a controller from processing more than one + IOCB simultaneously. + + .. method:: request_io(iocb) + + :param iocb: the IOCB to be processed + + This method is called by the application requesting the service of a + controller. + + .. method:: process_io(iocb) + + :param iocb: the IOCB to be processed + + The implementation of `process_io()` should be written using "functional + programming" principles by not modifying the arguments or keyword arguments + in the IOCB, and without side effects that would require the application + using the controller to submit IOCBs in a particular order. There may be + occasions following a "remote procedure call" model where the application + making the request is not in the same process, or even on the same machine, + as the controller providing the functionality. + + .. method:: active_io(iocb) + + :param iocb: the IOCB being processed + + This method is called by the derived class when it would like to signal + to other types of applications that the IOCB is being processed. + + .. method:: complete_io(iocb, msg) + + :param iocb: the IOCB to be processed + :param msg: the message to be returned + + This method is called by the derived class when the IO processing is + complete. The `msg`, which may be None, is put in the `ioResponse` + attribute of the IOCB which is then triggered. + + IOController derived classes should call this function rather than + the `complete()` function of the IOCB. + + .. method:: abort_io(iocb, msg) + + :param iocb: the IOCB to be processed + :param msg: the error to be returned + + This method is called by the derived class when the IO processing has + encountered an error. The `msg` is put in the `ioError` + attribute of the IOCB which is then triggered. + + IOController derived classes should call this function rather than + the `abort()` function of the IOCB. + + .. method:: abort(err) + + :param msg: the error to be returned + + This method is called to abort all of the IOCBs associated with the + controller. There is no default implementation of this method. + +.. class:: IOQueue + + An IOQueue is simply a first-in-first-out priority queue of IOCBs, but the + IOCBs are modified to know that they can been queued. If an IOCB is aborted + before being retrieved from the queue, it will ask the queue to remove it. + + .. method:: put(iocb) + + :param iocb: add an IOCB to the queue + + .. method:: get(block=1, delay=None) + + :param block: wait for an IOCB to be available in the queue + :param delay: maximum time to wait for an IOCB + + The `get()` request returns the next IOCB in the queue and waits for one + if there are none available. If `block` is false and the queue is + empty, it will return None. + + .. method:: remove(iocb) + + :param iocb: an IOCB to remove from the queue + + Removes an IOCB from the queue. If the IOCB is not in the queue, no + action is performed. + + .. method:: abort(err) + + :param msg: the error to be returned + + This method is called to abort all of the IOCBs in the queue. + +.. class:: IOQController + + An `IOQController` has an identical interface as the `IOContoller`, but + provides additional hooks to make sure that only one IOCB is being processed + at a time. + + .. method:: request_io(iocb) + + :param iocb: the IOCB to be processed + + This method is called by the application requesting the service of a + controller. If the controller is already busy processing a request, + this IOCB is queued until the current processing is complete. + + .. method:: process_io(iocb) + + :param iocb: the IOCB to be processed + + Provided by a derived class, this is identical to `IOController.process_io`. + + .. method:: active_io(iocb) + + :param iocb: the IOCB to be processed + + Called by a derived class, this is identical to `IOController.active_io`. + + .. method:: complete_io(iocb, msg) + + :param iocb: the IOCB to be processed + + Called by a derived class, this is identical to `IOController.complete_io`. + + .. method:: abort_io(iocb, msg) + + :param iocb: the IOCB to be processed + + Called by a derived class, this is identical to `IOController.abort_io`. + + .. method:: abort(err) + + :param msg: the error to be returned + + This method is called to abort all of the IOCBs associated with the + controller. All of the pending IOCBs will be aborted with this error. + +.. class:: IOGroup(IOCB) + + An `IOGroup` is like a set that is an IOCB. The group will complete + when all of the IOCBs that have been added to the group are complete. + + .. method:: add(iocb) + + :param iocb: an IOCB to include in the group + + Adds an IOCB to the group. + + .. method:: abort(err) + + :param err: the error to be returned + + This method is call to abort all of the IOCBs that are members of + the group. + + .. method:: group_callback(iocb) + + : param iocb: the member IOCB that has completed + + This method is added as a callback to all of the IOCBs that are added + to the group and it is called when each one completes. Its purpose + is to check to see if all of the IOCBs have completed and if they + have, trigger the group as completed. + +.. class:: IOChainMixIn + + The IOChainMixIn class adds an additional API to things that act like + an IOCB and can be mixed into the inheritance chain for translating + requests from one form to another. + + .. method:: __init__(iocb) + + :param iocb: the IOCB to chain from + + Create an object that is chained from some request. + + .. method:: encode() + + This method is called to transform the arguments and keyword arguments + into something suitable for the other controller. It is typically + overridden by a derived class to perform this function. + + .. method:: decode() + + This method is called to transform the result or error returned by + the other controller into something suitable to return. It is typically + overridden by a derived class to perform this function. + + .. method:: chain_callback(iocb) + + :param iocb: the IOCB that has completed, which is itself + + When a chained IOCB has completed, the results are translated or + decoded for the next higher level of the application. The `iocb` + parameter is redundant because the IOCB becomes its own controller, + but the callback API requires the parameter. + + .. method:: abort_io(iocb, err) + + :param iocb: the IOCB that is being aborted + :param err: the error to be used as the abort reason + + Call this method to abort the IOCB, which will in turn cascade the + abort operation to the chained IOCBs. This has the same function + signature that is used by an IOController because this instance + becomes its own controller. + +.. class:: IOChain(IOCB, IOChainMixIn) + + An IOChain is a class that is an IOCB that includes the IOChain API. + Chains are used by controllers when they need the services of some other + controller and results need to be processed further. + + Controllers that operate this way are similar to an adapter, they take + arguments in one form, encode them in some way in an IOCB, pass it to the + other controller, then decode the results. + +.. class:: ClientController(Client, IOQController) + + An instance of this class is a controller that sits at the top of a + protocol stack as a client. The IOCBs to be processed contain a single + PDU parameter that is sent down the stack. Any PDU coming back up + the stack is assumed to complete the current request. + + This class is used for protocol stacks with a strict master/slave + architecture. + + This class inherits from `IOQController` so if there is already an active + request then subsequent requests are queued. + +.. class:: _SieveQueue(IOQController) + + This is a special purpose controller used by the `SieveClientController` + to serialize requests for the same source/destination address. + +.. class:: SieveClientController(Client, IOController) + + Similar to the `ClientController`, this class is a controller that also + sits at the top of a protocol stack as a client. The IOCBs to be processed + contain a single PDU parameter with a `pduDestination` address. Unlike + the `ClientController`, this class creates individual queues for each + destination address so it can process multiple requests simultaneously while + maintaining a strict master/slave relationship with each address. + + When an upstream PDU is received, the `pduSource` address is used to + associate this response with the correct request. + +Functions +--------- + +.. function:: register_controller(controller) + + :param controller: controller to register + + The module keeps a dictionary of "registered" controllers so that other + parts of the application can find the controller instance. For example, + if an HTTP controller provided a GET service and it was registered then + other parts of the application could take advantage of the service the + controller provides. diff --git a/doc/source/modules/service/cov.rst b/doc/source/modules/service/cov.rst new file mode 100644 index 00000000..642efa96 --- /dev/null +++ b/doc/source/modules/service/cov.rst @@ -0,0 +1,170 @@ +.. BACpypes change of value services + +Change of Value (COV) Services +============================== + +.. class:: ChangeOfValueServices(Capability) + + This class provides the capability of managing COV subscriptions and + initiating COV notifications. + + .. method:: do_SubscribeCOVRequest(apdu): + + :param SubscribeCOVRequest apdu: request from the network + + This method processes the request by looking up the referenced object + and attaching a COV detection algorithm object. Any changes the to + referenced object properties (such as *presentValue* to *statusFlags*) + will trigger the algorithm to run and initiate COV notifications as + necessary. + + .. method:: add_subscription(cov) + + This method adds a subscription to the internal dictionary of subscriptions + indexed by the object reference. There can be multiple COV subscriptions + for the same object. + + .. method:: cancel_subscription(cov) + + This method removes a subscription from the internal dictionary of + subscriptions. If all of the subscriptinos have been removed, for + example they have all expired, then the detection "hook" into the + object is removed. + + .. method:: cov_notification(cov, request) + + This method is used to wrap a COV notification request in an + IOCB wrapper, submitting it as an IO request. The following confirmation + function will be called when it is complete. + + .. method:: cov_confirmation(iocb) + + This method looks at the response that was given to the COV notification + and dispatchs one of the following functions. + + .. method:: cov_ack(cov, request, response) + + This method is called when the client has responded with a simple + acknowledgement. + + .. method:: cov_error(cov, request, response) + + This method is called when the client has responded with an error. + Depending on the error, the COV subscription might be canceled. + + .. method:: cov_reject(cov, request, response) + + This method is called when the client has responded with a reject. + Depending on the error, the COV subscription might be canceled. + + .. method:: cov_abort(cov, request, response) + + This method is called when the client has responded with an abort. + Depending on the error, the COV subscription might be canceled. + + +Support Classes +--------------- + +.. class:: ActiveCOVSubscriptions(Property) + + An instance of this property is added to the local device object. When + the property is read it will return a list of COVSubscription objects. + + +.. class:: SubscriptionList + + .. method:: append(cov) + + :param Subscription cov: additional subscription + + .. method:: remove(cov) + + :param Subscription cov: subscription to remove + + .. method:: find(client_addr, proc_id, obj_id) + + :param Address client_addr: client address + :param int proc_id: client process identifier + :param ObjectIdentifier obj_id: object identifier + + This method finds a matching Subscription object where all three + parameters match. It is used when a subscription request arrives + it is used to determine if it should be renewed or canceled. + +.. class:: Subscription(OneShotTask) + + Instances of this class are active subscriptions with a lifetime. When the + subscription is created it "installs" itself as a task for the end of its + lifetime and when the process_task function is called the subscription + is canceled. + + .. method:: __init__(obj_ref, client_addr, proc_id, obj_id, confirmed, lifetime) + + :param obj_ref: reference to the object being monitored + :param client_addr: address of the client + :param proc_id: process id of the client + :param obj_id: object identifier + :param confirmed: issue confirmed notifications + :param lifetime: subscription lifetime + + .. method:: cancel_subscription() + + This method is called to cancel a subscription, it is called by + process_task. + + .. method:: renew_subscription(lifetime) + + :param int lifetime: seconds until expiration + + This method is called to renew a subscription. + + .. method:: process_task() + + Call when the lifetime of the subscription has run out. + +.. class:: COVDetection(DetectionAlgorithm) + + This is a base class for a series of COV detection algorithms. The derived + classes provide a list of the properties that are being monitored for + changes and a list of properties that are reported. + + .. method:: execute() + + This method overrides the execute function of the detection algorithm. + + .. method:: send_cov_notifications() + + This method sends out notifications to all of the subscriptions + that are associated with the algorithm. + +.. class:: GenericCriteria(COVDetection) + + This is the simplest detection algorithm that monitors the present value + and status flags of an object. + +.. class:: COVIncrementCriteria(COVDetection) + + This detection algorithm is used for those objects that have a COV increment + property, such as Analog Value Objects, where the change in the present + value needs to exceed some delta value. + +.. class:: AccessDoorCriteria(COVDetection) + + This detection algorithm is used for Access Door Objects. + +.. class:: AccessPointCriteria(COVDetection) + + This detection algorithm is used for Access Point Objects. + +.. class:: CredentialDataInputCriteria(COVDetection) + + This detection algorithm is used for Credential Data Input Objects. + +.. class:: LoadControlCriteria(COVDetection) + + This detection algorithm is used for Load Control Objects. + +.. class:: PulseConverterCriteria(COVDetection) + + This detection algorithm is used for Pulse Converter Objects. diff --git a/doc/source/modules/service/detect.rst b/doc/source/modules/service/detect.rst new file mode 100644 index 00000000..406ee586 --- /dev/null +++ b/doc/source/modules/service/detect.rst @@ -0,0 +1,103 @@ +.. BACpypes change detection module + +.. module:: detect + +Detect +====== + +This is a long line of text. + +Classes +------- + + +.. class:: DetectionMonitor + + .. attribute:: algorithm + .. attribute:: parameter + .. attribute:: obj + .. attribute:: prop + .. attribute:: filter + + .. method:: __init__(algorithm, parameter, obj, prop, filter=None) + + This is a long line of text. + + .. method:: property_change(old_value, new_value) + + This is a long line of text. + +.. class:: DetectionAlgorithm + + .. attribute:: _monitors + + This private attribute is a list of `DetectionMonitor` objects that + associate this algorithm instance with objects and properties. + + .. attribute:: _triggered + + This private attribute is `True` when there is a change in a parameter + which causes the algorithm to schedule itself to execute. More than + one parameter may change between the times that the algorithm can + execute. + + .. method:: __init__() + + Initialize a detection algorithm, which simply initializes the + instance attributes. + + .. method:: bind(**kwargs) + + :param tuple kwargs: parameter to property mapping + + Create a `DetectionMonitor` instance for each of the keyword arguments + and point it back to this algorithm instance. The algorithm parameter + matches the keyword parameter name and the parameter value is an + (object, property_name) tuple. + + .. method:: unbind() + + Delete the `DetectionMonitor` objects associated with this algorithm + and remove them from the property changed call list(s). + + .. method:: execute() + + This function is provided by a derived class which checks to see if + something should happen when its parameters have changed. For example, + maybe a change-of-value or event notification should be generated. + + .. method:: _execute() + + This method is a special wrapper around the `execute()` function + that sets the internal trigger flag. When the flag is set then the + `execute()` function is already scheduled to run (via `deferred()`) + and doesn't need to be scheduled again. + +Decorators +---------- + +.. function:: monitor_filter(parameter) + + :param string parameter: name of parameter to filter + + This decorator is used with class methods of an algorithm to determine + if the new value for a propert of an object is significant enough to + consider the associated parameter value changed. For example:: + + class SomeAlgorithm(DetectionAlgorithm): + + @monitor_filter('pValue') + def value_changed(self, old_value, new_value): + return new_value > old_value + 10 + + Assume that an instance of this algorithm is bound to the `presentValue` + of an `AnalogValueObject`:: + + some_algorithm = SomeAlgorithm() + some_algorithm.bind(pValue = (avo, 'presentValue')) + + The algorithm parameter `pValue` will only be considered changed when + the present value of the analog value object has increased by more than + 10 at once. If it slowly climbs by something less than 10, or declines + at all, the algorithm will not execute. + diff --git a/doc/source/modules/service/device.rst b/doc/source/modules/service/device.rst new file mode 100644 index 00000000..de79efbb --- /dev/null +++ b/doc/source/modules/service/device.rst @@ -0,0 +1,114 @@ +.. BACpypes device services + +Device Services +=============== + +.. class:: WhoIsIAmServices(Capability) + + This class provides the capability to initiate and respond to + device-address-binding PDUs. + + .. method:: do_WhoIsRequest(apdu) + + :param WhoIsRequest apdu: Who-Is Request from the network + + See Clause 16.10.1 for the parameters to this service. + + .. method:: do_IAmRequest(apdu) + + :param IAmRequest apdu: I-Am Request from the network + + See Clause 16.10.3 for the parameters to this service. + + .. method:: who_is(self, low_limit=None, high_limit=None, address=None) + + :param Unsigned low_limit: optional low limit + :param Unsigned high_limit: optional high limit + :param Address address: optional destination, defaults to a global broadcast + + This is a utility function that makes it simpler to generate a + `WhoIsRequest`. + + .. method:: i_am(self, address=None) + + :param Address address: optional destination, defaults to a global broadcast + + This is a utility function that makes it simpler to generate an + `IAmRequest` with the contents of the local device object. + +.. class:: WhoHasIHaveServices(Capability) + + This class provides the capability to initiate and respond to + device and object binding PDU's. + + .. method:: do_WhoHasRequest(apdu) + + :param WhoHasRequest apdu: Who-Has Request from the network + + See Clause 16.9.1 for the parameters to this service. + + .. method:: do_IHaveRequest(apdu) + + :param IHaveRequest apdu: I-Have Request from the network + + See Clause 16.9.3 for the parameters to this service. + + .. method:: who_has(thing, address=None) + + :param thing: object identifier or object name + :param Address address: optional destination, defaults to a global broadcast + + Not implemented. + + .. method:: i_have(thing, address=None) + + :param thing: object identifier or object name + :param Address address: optional destination, defaults to a global broadcast + + This is a utility function that makes it simpler to generate an + `IHaveRequest` given an object. + +Support Classes +--------------- + +There are a few support classes in this module that make it simpler to build +the most common BACnet devices. + +.. class:: CurrentDateProperty(Property) + + This class is a specialized readonly property that always returns the + current date as provided by the operating system. + + .. method:: ReadProperty(self, obj, arrayIndex=None) + + Returns the current date as a 4-item tuple consistent with the + Python implementation of the :class:`Date` primitive value. + + .. method:: WriteProperty(self, obj, value, arrayIndex=None, priority=None) + + Object instances of this class are readonly, so this method raises + a `writeAccessDenied` error. + +.. class:: CurrentTimeProperty(Property) + + This class is a specialized readonly property that always returns the + current local time as provided by the operating system. + + .. method:: ReadProperty(self, obj, arrayIndex=None) + + Returns the current date as a 4-item tuple consistent with the + Python implementation of the :class:`Time` primitive value. + + .. method:: WriteProperty(self, obj, value, arrayIndex=None, priority=None) + + Object instances of this class are readonly, so this method raises + a `writeAccessDenied` error. + +.. class:: LocalDeviceObject(DeviceObject) + + The :class:`LocalDeviceObject` is an implementation of a + :class:`DeviceObject` that provides default implementations for common + properties and behaviors of a BACnet device. It has default values for + communications properties, returning the local date and time, and + the `objectList` property for presenting a list of the objects in the + device. diff --git a/doc/source/modules/service/file.rst b/doc/source/modules/service/file.rst new file mode 100644 index 00000000..87322eff --- /dev/null +++ b/doc/source/modules/service/file.rst @@ -0,0 +1,84 @@ +.. BACpypes file services + +File Services +============= + +.. class:: FileServices(Capability) + + This class provides the capability to read from and write to file objects. + + .. method:: do_AtomicReadFileRequest(apdu) + + :param AtomicReadFileRequest apdu: request from the network + + This method looks for a local file object by the object identifier + and and passes the request parameters to the implementation of + the record or stream support class instances. + + .. method:: do_AtomicWriteFileRequest(apdu) + + :param AtomicWriteFileRequest apdu: request from the network + + This method looks for a local file object by the object identifier + and and passes the request parameters to the implementation of + the record or stream support class instances. + +Support Classes +--------------- + +.. class:: LocalRecordAccessFileObject(FileObject) + + This abstract class provides a simplified API for implementing a local + record access file. A derived class must provide implementations of + these methods for the object to be used by the `FileServices`. + + .. method:: __len__() + + Return the length of the file in records. + + .. method:: read_record(start_record, record_count) + + :param int start_record: starting record + :param int record_count: number of records + + Return a tuple (eof, record_data) where the `record_data` is an + array of octet strings. + + .. method:: write_record(start_record, record_count, record_data) + + :param int start_record: starting record + :param int record_count: number of records + :param record_data: array of octet strings + + Update the file with the new records. + +.. class:: LocalStreamAccessFileObject(FileObject) + + This abstract class provides a simplified API for implementing a local + stream access file. A derived class must provide implementations of + these methods for the object to be used by the `FileServices`. + + .. method:: __len__() + + Return the length of the file in octets. + + .. method:: read_stream(start_position, octet_count) + + :param int start_position: starting position + :param int octet_count: number of octets + + Return a tuple (eof, record_data) where the `record_data` is an + array of octet strings. + + .. method:: write_stream(start_position, data) + + :param int start_position: starting position + :param data: octet string + + Update the file with the new records. + +.. class:: FileServicesClient(Capability) + + This class adds a set of functions to the application that provides a + simplified client API for reading and writing to files. It is not currently + implemented. diff --git a/doc/source/modules/service/index.rst b/doc/source/modules/service/index.rst new file mode 100644 index 00000000..6131d0ad --- /dev/null +++ b/doc/source/modules/service/index.rst @@ -0,0 +1,20 @@ +.. BACpypes service modules + +Service Modules +--------------- + +.. toctree:: + :maxdepth: 2 + + device.rst + object.rst + file.rst + +Change Detection and Reporting +------------------------------ + +.. toctree:: + :maxdepth: 2 + + detect.rst + cov.rst diff --git a/doc/source/modules/service/object.rst b/doc/source/modules/service/object.rst new file mode 100644 index 00000000..3de0542d --- /dev/null +++ b/doc/source/modules/service/object.rst @@ -0,0 +1,62 @@ +.. BACpypes object services + +Object Services +=============== + +.. class:: ReadWritePropertyServices(Capability) + + This class provides the capability to respond to ReadProperty and + WriteProperty service, used by a client BACnet-user to request the value + of one property of one BACnet Object. + + .. method:: do_ReadPropertyRequest(apdu) + + :param ReadPropertyRequest apdu: request from the network + + See Clause 15.5 for the parameters to this service. + + .. method:: do_WritePropertyRequest(apdu) + + :param WritePropertyRequest apdu: request from the network + + See Clause 15.9 for the parameters to this service. + +.. class:: ReadWritePropertyMultipleServices(Capability) + + This class provides the capability to respond to ReadPropertyMultiple and + WritePropertyMultiple service, used by a client BACnet-user to request the + values of one or more specified properties of one or more BACnet Objects. + + .. method:: do_ReadPropertyMultipleRequest(apdu) + + :param ReadPropertyRequest apdu: request from the network + + See Clause 15.7 for the parameters to this service. + + .. method:: do_WritePropertyMultipleRequest(apdu) + + :param WritePropertyMultipleRequest apdu: request from the network + + Not implemented. + +Support Functions +----------------- + + .. function:: read_property_to_any(obj, propertyIdentifier, propertyArrayIndex=None): + + :param obj: object + :param propertyIdentifier: property identifier + :param propertyArrayIndex: optional array index + + Called by `read_property_to_result_element` to build an appropriate + `Any` result object from the supplied object given the property + identifier and optional array index. + + .. function:: read_property_to_result_element(obj, propertyIdentifier, propertyArrayIndex=None): + + :param obj: object + :param propertyIdentifier: property identifier + :param propertyArrayIndex: optional array index + + Called by `do_ReadPropertyMultipleRequest` to build the result element + components of a `ReadPropertyMultipleACK`. diff --git a/doc/source/modules/singleton.rst b/doc/source/modules/singleton.rst index 4a1f2050..36a1d177 100644 --- a/doc/source/modules/singleton.rst +++ b/doc/source/modules/singleton.rst @@ -6,9 +6,9 @@ Singleton ========= Singleton classes are a `design pattern `_ -that returns the same object for every call to create an instance. In the case +which returns the same object for every 'create an instance' call. In the case of BACpypes there can only be one instance of a :class:`task.TaskManager` and -all of the tasks will be schedule through it. The design pattern "hides" all +all of the tasks are scheduled through it. The design pattern "hides" all of the implementation details of the task manager behind its interface. There are occasions when the task manager needs to provide additional diff --git a/doc/source/samples/images/RandomAnalogValue.png b/doc/source/samples/images/RandomAnalogValue.png new file mode 100644 index 00000000..b02d7180 Binary files /dev/null and b/doc/source/samples/images/RandomAnalogValue.png differ diff --git a/doc/source/samples/sample001.rst b/doc/source/samples/sample001.rst index 6f812672..2d53eabe 100644 --- a/doc/source/samples/sample001.rst +++ b/doc/source/samples/sample001.rst @@ -1,4 +1,3 @@ -.. BACpypes tutorial lesson 1 Sample 1 - Simple Application ============================= @@ -15,17 +14,16 @@ 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:: +Debugging and logging is brought to the application via a decorator (see later in class) and +you will need :class:`debugging.ModuleLogger`:: - import sys - import logging - from ConfigParser import ConfigParser + from bacpypes.debugging import bacpypes_debugging, ModuleLogger -Immediately following the built-in module includes are those for debugging:: +All BACpypes applications gather some options from the command line and use the +:class:`consolelogging.ConfigArgumentParser` function for reading configuration +information:: - from bacpypes.debugging import Logging, ModuleLogger - from bacpypes.consolelogging import ConsoleLogHandler + from bacpypes.consolelogging import ConfigArgumentParser For applications that communicate on the network, it needs the :func:`core.run` function:: @@ -35,10 +33,10 @@ function:: 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`:: +:class:`service.device.LocalDeviceObject`:: from bacpypes.app import BIPSimpleApplication - from bacpypes.object import LocalDeviceObject + from bacpypes.service.device import LocalDeviceObject Global variables are initialized before any other classes or functions:: @@ -46,139 +44,172 @@ Global variables are initialized before any other classes or functions:: _debug = 0 _log = ModuleLogger(globals()) -Now skipping down to the main block. Everything is wrapped in a +Now skipping down to the main function. 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 +shutdown notifications to other processes and it is important to include the exception (or graceful conclusion) of the application along with the notification:: # # __main__ # - - try: + + 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() - _log.debug("initialization") - # code goes here... +Generic Initialization +---------------------- - _log.debug("running") - # code goes here... +These sample applications and other server applications are run on many machines +on a BACnet intranet so INI files are used for configuration parameters. - except Exception, e: - _log.exception("an error has occurred: %s", e) - finally: - _log.debug("finally") +.. 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. The application simply -quits after the list:: +been imported, and sometimes it's easy to get lost.:: - 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) + # 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__ -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] +Will output:: -Usually the debugging hooks will be added to the end of the parameter and option -list:: + __main__ + __main__.SampleApplication - $ python sample001.py --debug __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. Usually the debugging hooks will be added to the end of the +parameter and option list:: -Generic Initialization ----------------------- + $ python SampleApplication.py --debug __main__ -These sample applications and other server applications are run on many machines -on a BACnet intranet so INI files are used for configuration parameters. +Will output:: -.. 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. + DEBUG:__main__:initialization + DEBUG:__main__: - args: Namespace(buggers=False, color=False, debug=['__main_ + _'], ini=) + DEBUG:__main__:running + DEBUG:__main__:fini -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 +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 - 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') - ) + 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 test application - SampleApplication(thisDevice, config.get('BACpypes','address')) + # 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() + run() -Sample Application ------------------- +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 statements at the front of the method calls:: +usual debugging decorator.:: # # SampleApplication # - class SampleApplication(BIPSimpleApplication, Logging): + @bacpypes_debugging + class SampleApplication(BIPSimpleApplication): def __init__(self, device, address): if _debug: SampleApplication._debug("__init__ %r %r", device, address) @@ -220,37 +251,87 @@ 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 + $ python SampleApplication.py So to see what is actually happening, run the application with debugging enabled:: - $ python sample001.py --debug __main__ + $ 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=) + DEBUG:__main__.SampleApplication:__init__ '192.168.1.2/24' + DEBUG:__main__: - this_application: <__main__.SampleApplication object at 0x0 + 00000000301FC88> + DEBUG:__main__: - services_supported: + DEBUG:__main__:running -The output will include the initialization, running, and finally statements. To -run with debugging on just the SampleApplication class:: +To run with debugging on just the SampleApplication class:: - $ python sample001.py --debug __main__.SampleApplication + $ python SampleApplication.py --debug __main__.SampleApplication -Or to see what is happening at the UDP layer of the program, use that module -name:: +Will output:: - $ python sample001.py --debug bacpypes.udp + DEBUG:__main__.SampleApplication:__init__ '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= 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 sample001.py --debug bacpypes.udp.UDPActor + $ 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 sample001.py --debug bacpypes.udp.UDPActor __main__.SampleApplication + $ 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, and you can see these +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` -burried in the stack:: +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 - $ python sample001.py --debug bacpypes.netservice.NetworkServiceElement +The definition of debug:: + positional arguments: + --debug [DEBUG [ DEBUG ... ]] + DEBUG ::= debugger [ : fileName [ : maxBytes [ : backupCount ]]] \ No newline at end of file diff --git a/doc/source/samples/sample002.rst b/doc/source/samples/sample002.rst index 70c75e37..4d7d7d03 100644 --- a/doc/source/samples/sample002.rst +++ b/doc/source/samples/sample002.rst @@ -1,5 +1,3 @@ -.. BACpypes tutorial lesson 1 - Sample 2 - Who-Is/I-Am Counter ============================== @@ -10,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 -------- @@ -33,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:: @@ -50,7 +58,7 @@ The middle is going to process the data in the request:: ) # count the times this has been received - whoIsCounter[key] += 1 + who_is_counter[key] += 1 And the end of the function is going to call back to the standard application processing:: @@ -58,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.""" @@ -72,7 +80,7 @@ It uses a diferent key, but counts them the same:: ) # count the times this has been received - iAmCounter[key] += 1 + i_am_counter[key] += 1 And has an identical call to the base class:: @@ -86,12 +94,58 @@ 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 -----" - for (src, lowlim, hilim), count in sorted(whoIsCounter.items()): - print "%-20s %8s %8s %4d" % (src, lowlim, hilim, count) - print + print("----- Who Is -----") + for (src, lowlim, hilim), count in sorted(who_is_counter.items()): + print("%-20s %8s %8s %4d" % (src, lowlim, hilim, count)) + print("") -Pairing up the requests and responses can be a useful excersize, but in most +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 to the requestor, so relying on broadcast traffic to analyze device and address binding is not as useful as it used to be. + +Running the Application +----------------------- + +:: + + $ python WhoIsIAmApplication.py --debug __main__ + + DEBUG:__main__:initialization + DEBUG:__main__: - args: Namespace(buggers=False, color=False, debug=['__main__'], ini=) + DEBUG:__main__.WhoIsIAmApplication:__init__ '192.168.87.59/24' + DEBUG:__main__: - services_supported: + DEBUG:__main__:running + +Let it run for a minute, then Press to end it. It will output its results.:: + + DEBUG:__main__.WhoIsIAmApplication:do_WhoIsRequest + + pduSource =
+ pduDestination = + pduExpectingReply = False + pduNetworkPriority = 0 + apduType = 1 + apduService = 8 + deviceInstanceRangeLowLimit = 59L + deviceInstanceRangeHighLimit = 59L + pduData = x'' + [clipped...] + DEBUG:__main__:fini + ----- Who Is ----- + 10001:0x0040ae007e01 1 1 1 + 10001:0x0040ae007e01 9830 9830 1 + 10001:0x005008067649 536 536 1 + 10001:0x005008067649 2323 2323 1 + 192.168.87.115 9 9 3 + 192.168.87.115 59 59 1 + 192.168.87.115 226 226 3 + 192.168.87.115 900 900 2 + 192.168.87.115 11189 11189 3 + 192.168.87.115 80403 80403 3 + 192.168.87.115 110900 110900 3 + 192.168.87.115 4194302 4194302 2 + 192.168.87.48 3300 3300 1 + + ----- I Am ----- + diff --git a/doc/source/samples/sample003.rst b/doc/source/samples/sample003.rst index 44d98467..feeedd06 100644 --- a/doc/source/samples/sample003.rst +++ b/doc/source/samples/sample003.rst @@ -1,4 +1,3 @@ -.. BACpypes tutorial lesson 1 Sample 3 - Who-Has/I-Have Counter ================================= @@ -6,7 +5,7 @@ Sample 3 - Who-Has/I-Have Counter This sample application is very similar to the second sample. It has the same basic structure and initialization, it counts the number of Who-Has and I-Have messages it receives, and prints out a summary after the application -has been signaled to terminate, such as a KeyboardInterrupt raised. +has been signalled to terminate ( - KeyboardInterrupt). Processing Service Requests @@ -34,23 +33,23 @@ cannot appear in the APDU at the same time:: return # count the times this has been received - whoHasCounter[key] += 1 + who_has_counter[key] += 1 -When an optional parameter is not specified in a PDU then the corrisponding -attribute will be ``None``. With this particular APDU the *object* -parameter is required, but only one of its child attributes *objectIdentifier* +When an optional parameter is not specified in a PDU then the corresponding +attribute is ``None``. With this particular APDU the *object* +parameter is required, and one of its child attributes *objectIdentifier* or *objectName* will be not ``None``. If both are ``None`` then the request is not properly formed. .. note:: - The encoding and decoding layer will not completely understand all of + The encoding and decoding layer does not understand all the combinations of required and optional parameters in an APDU, so - verify the validity of the reuest is the responsibility of the application. + verify the validity of the request is the responsibility of the application. - The application can rely on the fact that the APDU is well-formed, which - is to say it has teh appropriate opening and closing tags and the data - types of the parameters are correct. Watch out for Any! + The application can rely on the fact that the APDU is well-formed - meaning + it has the appropriate opening and closing tags and the data + types of the parameters are correct. Watch out for parameters of type Any! The I-Am function is much simpler because all of the parameters are required:: @@ -62,13 +61,36 @@ The I-Am function is much simpler because all of the parameters are required:: ) # count the times this has been received - iHaveCounter[key] += 1 + i_have_counter[key] += 1 Dumping the contents of the counters is simple. Just like Who-Is and I-Am, pairing up the requests and responses can be a -useful excersize, but in most cases the I-Am response from a device will be a +useful exercize, but in most cases the I-Am response from a device will be a unicast message directly back to the requestor, so relying on broadcast traffic to analyze object binding is not as useful as it used to be. -The Who-Has and I-Have services are not widely used. +Running the Application +----------------------- + +:: + + $ python WhoHasIHaveApplication.py --debug __main__ + + DEBUG:__main__:initialization + DEBUG:__main__: - args: Namespace(buggers=False, color=False, debug=['__main__'], ini=) + DEBUG:__main__.WhoHasIHaveApplication:__init__ '192.168.87.59/24' + DEBUG:__main__: - services_supported: + DEBUG:__main__:running + +Allow the application to run for a few minutes. Then end it so it will output its results.:: + + DEBUG:__main__:fini + ----- Who Has ----- + + ----- I Have ----- + +.. note:: + + The Who-Has and I-Have services are not widely used. + diff --git a/doc/source/samples/sample004.rst b/doc/source/samples/sample004.rst index db0cd391..636b024c 100644 --- a/doc/source/samples/sample004.rst +++ b/doc/source/samples/sample004.rst @@ -4,7 +4,7 @@ Sample 4 - Extending Objects and Properties =========================================== This sample application shows how to extend one of the basic objects, an Analog -Value Object in this case, to provide a custom property, the present value. +Value Object in this case, to provide a custom property - present value. This type of code is used when the application is providing a BACnet interface to a collection of data. It assumes that almost all of the default behaviour of a BACpypes application is sufficient. @@ -12,21 +12,20 @@ of a BACpypes application is sufficient. .. note:: The code in this description starts at the __main__ block and goes - backward through the source file. + backward through the source file - RandomAnalogValueObject.py. Constructing the Device ----------------------- Initialization is simple, the simple BACnet/IP application, which includes the -networking layer and communications layers all bundled in together is created +networking layer and communications layers all bundled together is created like the other samples:: # make a sample application thisApplication = BIPSimpleApplication(thisDevice, config.get('BACpypes','address')) -The only object this has by default is an instance of a -:class:`object.LocalDeviceObject`. The next step is to create a special Analog -Value Object and add it to the application:: +The only object by default is an instance of :class:`object.LocalDeviceObject`. +The next step is to create a special Analog Value Object and add it to the application:: # make a random input object raio = RandomAnalogValueObject(objectIdentifier=('analog-value', 1), objectName='Random') @@ -47,7 +46,7 @@ will make sure the keyword argument value is appropriate for the property. Extending the Analog Value Object --------------------------------- -The definition of a new kind of Analog Value Object uses Python inhertiance, +The definition of a new kind of Analog Value Object uses Python inheritance, so it seems fairly simple:: class RandomAnalogValueObject(AnalogValueObject): @@ -66,25 +65,24 @@ so this registration is going to override it. Overriding the same base type, like AnalogValueObject, in more than one way could be difficult in some kinds of gateway software which may require re-factoring the :func:`object.get_object_class` and the - :func:`object.get_datatype` functionality. This will be addressed before - BACpypes reaches v1.0. + :func:`object.get_datatype` functionality. The first part of :func:`object.register_object_type` builds a dictionary of -a relationship between the property name and its associated instance. It will +the relationship between the property name and its associated instance. It will look for *properties* class attribtues in the entire inheritance tree (using the method resolution order) but only associate the first of two instances with the same name. So in this case, the RandomValueProperty instance called 'present-value' will -be bound to the object type before the built-in version it finds in the +be bound to the object type before the built-in version finds it in the *properties* list in the :class:`object.AnalogValueObject`. A New Real Property ------------------- -BACnet clients will expect that the 'present-value' of an Analog Value Object -will be :class:`primitivedata.Real` and returning some other datatype would -seriously break interperabililty. The initialization is almost identical +BACnet clients expect the 'present-value' of an Analog Value Object +to be :class:`primitivedata.Real` and returning some other datatype would +seriously break interperabililty! Initialization is almost identical to the one for the built-in AnalogValueObject:: class RandomValueProperty(Property, Logging): @@ -97,8 +95,8 @@ to the one for the built-in AnalogValueObject:: The only difference is *mutable* is ``False``, which means BACnet clients will receive an error if they attempt to write a value to the property. -The core of the application is responding to a ReadPropertyRequest, which is -mapped into a ReadProperty function call:: +The core of the application is responding to a ReadPropertyRequest +(mapped to a ReadProperty function call):: def ReadProperty(self, obj, arrayIndex=None): @@ -106,7 +104,7 @@ mapped into a ReadProperty function call:: if arrayIndex is not None: raise Error(errorClass='property', errorCode='property-is-not-an-array') -The **arrayIndex** parameter will be some integer value if the BACnet client is +The **arrayIndex** parameter is an integer value if the BACnet client is accessing the property as an array, which is an error. Now it comes down to getting a random value and returning it:: @@ -119,3 +117,17 @@ getting a random value and returning it:: The value returned by this function will be passed as an initial value to construct a :class:`primitivedata.Real` object, which will then be encoded into the :class:`apdu.ReadPropertyACK` response and returned to the client. + +Running the Application +------------------------ + +:: + + $ python RandonAnalogValue.py + +Then using a BACnet client - like an OWS (Operator Workstation) or BACnet exploration +tool, read the application's Analog Value Objects. Notice: the value of the Present Value +property changes each time it is read by the client tool. + +.. image:: images/RandomAnalogValue.png + diff --git a/doc/source/samples/sample014.rst b/doc/source/samples/sample014.rst index def04995..b35ee45c 100644 --- a/doc/source/samples/sample014.rst +++ b/doc/source/samples/sample014.rst @@ -3,8 +3,8 @@ Sample 14 - Getting External Data ================================= -This is a pair of sample applications, a server that provides key:value updates -in the form of JSON objects, and a client that periodically polls the server +This is a pair of sample applications: a server that provides key:value updates +in the form of JSON objects; and a client that periodically polls the server for updates and applies them to a cache. Server Code diff --git a/doc/source/tutorial/capability.rst b/doc/source/tutorial/capability.rst new file mode 100644 index 00000000..af5a9435 --- /dev/null +++ b/doc/source/tutorial/capability.rst @@ -0,0 +1,101 @@ +.. BACpypes capability tutorial + +Capabilities +============ + +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. + +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 + +Transforming Data +----------------- + +Assume that the application needs to transform data in a variety of different +ways, but the exact order of those functions isn't specified, but all of the +transformation functions have the same signature. + +First, create a class that is going to be the foundation of the transformation +process:: + + class BaseCollector(Collector): + + def transform(self, value): + for fn in self.capability_functions('transform'): + value = fn(self, value) + + return value + +If there are no other classes mixed in, the `transform()` function doesn't +do anything:: + + >>> some_transformer = BaseCollector() + >>> some_transformer.transform(10) + 10 + +Adding a Transformation +----------------------- + +Create a `Capability` derived class that transforms the value slightly:: + + class PlusOne(Capability): + + def transform(self, value): + return value + 1 + +Now create a new class that mixes in the base collector:: + + class ExampleOne(BaseCollector, PlusOne): + pass + +And our transform function incorporates the new behavior:: + + >>> some_transformer = ExampleOne() + >>> some_transformer.transform(10) + 11 + +Add Another Transformation +-------------------------- + +Here is a different transformation class:: + + class TimesTen(Capability): + + def transform(self, value): + return value * 10 + +And the new class works as intended:: + + class ExampleTwo(BaseCollector, TimesTen): + pass + + >>> some_transformer = ExampleTwo() + >>> some_transformer.transform(10) + 100 + +And the classes can be mixed in together: + + class ExampleThree(BaseCollector, PlusOne, TimesTen): + pass + + >>> some_transformer = ExampleThree() + >>> some_transformer.transform(10) + 110 + +The order of the classes makes a difference:: + + class ExampleFour(BaseCollector, TimesTen, PlusOne): + pass + + >>> some_transformer = ExampleFour() + >>> some_transformer.transform(10) + 101 + diff --git a/doc/source/tutorial/iocb.rst b/doc/source/tutorial/iocb.rst new file mode 100644 index 00000000..034a03eb --- /dev/null +++ b/doc/source/tutorial/iocb.rst @@ -0,0 +1,135 @@ +.. BACpypes IOCB tutorial + +Controllers and IOCB +==================== + +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. + +For this tutorial section, import the IOCB and IOController:: + + >>> from bacpypes.iocb import IOCB, IOController + +Building an IOCB +---------------- + +Build an IOCB with some arguments and keyword arguments:: + + >>> iocb = IOCB(1, 2, a=3) + +The parameters are kept for processing:: + + >>> iocb.args + (1, 2) + >>> iocb.kwargs + {'a': 3} + +Make a Controller +----------------- + +Now we need a controller to process this request. This controller is just +going to add and multiply the arguments together:: + + class SomeController(IOController): + + def process_io(self, iocb): + self.complete_io(iocb, iocb.args[0] + iocb.args[1] * iocb.kwargs['a']) + +Now create an instance of the controller and pass it the request:: + + >>> some_controller = SomeController() + >>> some_controller.request_io(iocb) + +First, you'll notice that `request_io()` was called rather than the processing +function directly. This intermediate layer between the caller of the service +and the thing providing the service can be detached from each other in a +variety of different ways. + +For example, there are some types of controllers that can only process one +request at a time and these are derived from `IOQController`. If the application +layer requests IOCB processing faster than the controller can manage (perhaps +because it is waiting for some networking functions) the requests will be queued. + +In other examples, the application making the request is in a different process +or on a different machine, so the `request_io()` function builds a remote +procedure call wrapper around the request and manages the response. This is +similar to an HTTP proxy server. + +Similarly, inside the controller it calls `self.complete_io()` so if there is +some wrapper functionality the code inside the `process_io()` function doesn't +need to worry about it. + +Check the Result +---------------- + +There are a few ways to check to see if an IOCB has been processed. Every +IOCB has an `Event` from the `threading` built in module, so the application +can check to see if the event is set:: + + >>> iocb.ioComplete + + >>> iocb.ioComplete.is_set() + True + +There is also an IOCB state which has one of a collection of enumerated values:: + + >>> import bacpypes + >>> iocb.ioState == bacpypes.iocb.COMPLETED + True + +And the state could also be aborted:: + + >>> iocb.ioState == bacpypes.iocb.ABORTED + False + +Almost all controllers return some kind of information back to the requestor +in the form of some data. In this example, it's just a number:: + + >>> iocb.ioResponse + 7 + +But we can provide some invalid combination of arguments and the exception +will show up in the `ioError`:: + + >>> iocb = IOCB(1, 2) + >>> some_controller.request_io(iocb) + >>> iocb.ioError + KeyError('a',) + +The types of results and errors depend on the controller. + +Getting a Callback +------------------ + +When a controller completes the processing of a request, the IOCB can contain +one or more functions to be called. First, define a callback function:: + + def call_me(iocb): + print("call me, %r or %r" % (iocb.ioResponse, iocb.ioError)) + +Now create a request and add the callback function:: + + >>> iocb = IOCB(1, 2, a=10) + >>> iocb.add_callback(call_me) + +Pass the IOCB to the controller and the callback function is called:: + + >>> some_controller.request_io(iocb) + call me, 21 or None + +Threading +--------- + +The IOCB module is thread safe, but the IOController derived classes may +not be. The thread initiating the request to the controller may simply +wait for the completion event to be set:: + + >>> some_controller.request_io(iocb) + >>> iocb.ioComplete.wait() + +But for this to work correctly, the IOController must be running in a +separate thread, or there won't be any way for the event to be set. + +If the iocb has callback functions, they will be executed in the thread +context of the controller. diff --git a/doc/source/tutorial/tutorial001.rst b/doc/source/tutorial/tutorial001.rst index b83f05a8..d542c20d 100644 --- a/doc/source/tutorial/tutorial001.rst +++ b/doc/source/tutorial/tutorial001.rst @@ -5,40 +5,41 @@ 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:: >>> from bacpypes.comm import Client, Server, bind -This is a long line of text. - Since the server needs to do something when it gets a request, it needs to provide a function to get it:: >>> class MyServer(Server): ... def indication(self, arg): - ... print "working on", arg + ... print('working on', arg) ... self.response(arg.upper()) ... Now create an instance of this new class and bind the client and server together:: + >>> c = Client() >>> s = MyServer() >>> bind(c, s) This only solves the downstream part of the problem, as you can see:: - >>> c.request("hi") - working on hi + >>> c.request('hi') + ('working on ', 'hi') + Traceback.... + .... NotImplementedError: confirmation must be overridden So now we create a custom client class that does something with the response:: >>> class MyClient(Client): ... def confirmation(self, pdu): - ... print "thanks for the", pdu + ... print('thanks for the ', pdu) ... Create an instance of it, bind the client and server together and test it:: @@ -46,7 +47,7 @@ Create an instance of it, bind the client and server together and test it:: >>> c = MyClient() >>> bind(c, s) >>> c.request('hi') - working on hi - thanks for the HI + ('working on ', 'hi') + ('thanks for ', 'HI') Success! diff --git a/doc/source/tutorial/tutorial002.rst b/doc/source/tutorial/tutorial002.rst index dceec145..a76bfa05 100644 --- a/doc/source/tutorial/tutorial002.rst +++ b/doc/source/tutorial/tutorial002.rst @@ -4,55 +4,57 @@ Stacking with Debug =================== This tutorial uses the same :class:`comm.Client`, :class:`comm.Server` classes -from the previous one, so continuing on all it needs is the :class:`comm.Debug` -class, so import it:: +from the previous one, so continuing on from previous tutorial, all we need is +to import the class:`comm.Debug`:: >>> from bacpypes.comm import Debug -Because there could be lots of Debug instances, it could be confusing if you -didn't know which instance was generating the output. So you can initialize -an instance with a lobel:: +Because there could be lots of **Debug** instances, it could be confusing if you +didn't know which instance was generating the output. So initialize the debug +instance with a name:: >>> d = Debug("middle") As you can guess, this is going to go into the middle of a :term:`stack` of objects. The *top* of the stack is a client, then *bottom* of a stack is a server. When messages are flowing from clients to servers they are called -:term:`downstream` messages, and when they go from server to the client they go -:term:`upstream`. +:term:`downstream` messages, and when they flow from server to client they +are :term:`upstream` messages. -The :func:`comm.bind` function takes an arbitrary number of objects, but it +The :func:`comm.bind` function takes an arbitrary number of objects. It assumes that the first one will always be a client, the last one is a server, -and that the objects in the middle are both a kind of server that can be -bound with the client to its left in the parameter list, and a client that can -be bound to a server to its right:: +and the objects in the middle are hybrids which can be +bound with the client to its left, and to the server on its right:: >>> bind(c, d, s) Now when the client generates a request, rather than the message being sent -to the MyServer instance, it is sent to the debugging instance. That is acting -as a server, so it prints out that it received an indication:: +to the MyServer instance, it is sent to the debugging instance, which +prints out that it received the message:: >>> c.request('hi') Debug(middle).indication - args[0]: hi -Now it acts as a client and forwards it down to the server in the stack. That -generates a print statement and responds with the string uppercase:: +The debugging instance then forwards the message to the server, which prints +its message. Completeing the requests *downstream* journey.:: working on hi -Upstream from the server is the debugging instance again, this time as a -confirmation:: +The server then generates a reply. The reply moves *upstream* from the server, +through the debugging instance, this time as a confirmation:: Debug(middle).confirmation - args[0]: HI -Now it acts as a server and continues the response up the stack, which is -printed out by the client:: +Which is then forwarded *upstream* to the client:: thanks for the HI +This demonstrates how requests first move *downstream* from client to server; then +cause the generation of replies that move *upstream* from server to client; and how the +debug instance in the middle sees the messages moving both ways. + With clearly defined "envelopes" of protocol data, matching the combination of clients and servers into layers can provide a clear separation of functionality in a protocol stack. diff --git a/doc/source/tutorial/tutorial003.rst b/doc/source/tutorial/tutorial003.rst index 91c166a9..70178e7d 100644 --- a/doc/source/tutorial/tutorial003.rst +++ b/doc/source/tutorial/tutorial003.rst @@ -9,8 +9,8 @@ According to `Wikipedia `_ a Information that is delivered as a unit among peer entities of a network and that may contain control information, address information, or data. -BACpypes uses a slght variation of this definition in that it bundles the -address information in with the control information. It considers addressing +BACpypes uses a slight variation of this definition in that it bundles the +address information with the control information. It considers addressing as part of how the data should be delivered, along with other concepts like how important the PDU data is relative to other PDUs. @@ -18,63 +18,87 @@ The basic components of a PDU are the :class:`comm.PCI` and :class:`comm.PDUData` classes which are then bundled together to form the :class:`comm.PDU` class. -All of the protocol interpreters that have been written in the course of -developing BACpypes have all had at least some concept of source and +All of the protocol interpreters written in the course of +developing BACpypes have a concept of source and destination. The :class:`comm.PCI` defines only two attributes, **pduSource** and **pduDestination**. -Only in the case of pure master/slave networks has only the destination -encoded by the master to direct it to a specific slave (so source information -is implicit and not encoded) and the response from the slave back to the master -(so no addressing is included at all). These special cases are rare. +.. note:: + Master/slave networks, are an exception. Messages sent by the master, contain + only the destination (the source is implicit). Messages returned by the slaves + have no addressing (both the source, and destination are implicit). + As a foundation layer, there are no restrictions on the form of the source and destination, they could be integers, strings or even objects. In general, the :class:`comm.PDU` class is used as a base class for a series of stack -specific components, so UDP traffic will have combinations of IP addresses and +specific components. UDP traffic have combinations of IP addresses and port numbers as source and destination, then that will be inherited by something that provides more control information, like delivery order or priority. -Beginning with the base class:: + +Exploring PDU's +--------------- + +Begin with importing the base class:: >>> from bacpypes.comm import PDU -While source and destination are defined in the PCI, they are optional keyword -parameters. Debugging the contents of the PDU will skip over those attributes -that are ``None`` and strings are assumed to be a sequence of octets and so -are printed as hex encoded strings:: +Create a new PDU with some simple content:: + + >>> pdu = PDU(b"hello") + +.. caution:: + + If you are not using Python 3, you don't need to specify the bytes type. + >>> pdu = PDU("Hello") + +We can then see the contents of the PDU as it will be seen on the network +wire and by Wireshark - as a sequence of octets (printed as hex encoded strings):: - >>> pdu = PDU("hello") >>> pdu.debug_contents() pduData = x'68.65.6C.6C.6F' -Now add some source and destination information:: +Now lets add some source and destination addressing information, so the message +can be sent somewhere:: - >>> pdu = PDU("hello", source=1, destination=2) + >>> pdu.pduSource = 1 + >>> pdu.pduDestination = 2 + >>> pdu.debug_contents() + pduSource = 1 + pduDestination = 2 + pduData = x'68.65.6c.6c.6f' + +Of course, we could have provided the addressing information when we created the PDU:: + + >>> pdu = PDU(b"hello", source=1, destination=2) >>> pdu.debug_contents() pduSource = 1 pduDestination = 2 pduData = x'68.65.6C.6C.6F' -It is customary to allow missing attributes (which is protocol control -information or it would be data) to allow the developer to mixed keyword -parameters and post-init attribute assignment. +.. tip:: + + It is customary to allow missing attributes (be it protocol control + information or data) as this allows the developer to mix keyword + parameters with post-init attribute assignments. + BACnet PDUs ----------- -The PDU definition in the core is fine for many protocols, but BACnet has two -additional protocol parameters, described as attributes of a BACnet PCI +The basic PDU definition is fine for many protocols, but BACnet has two +additional protocol parameters, described as attributes of the BACnet PCI information. The :class:`pdu.PCI` class extends the basic PCI with **pduExpectingReply** and **pduNetworkPriority**. The former is only used in MS/TP networks so the node generating the request will not pass the token before waiting some amount -of time for a response, and the latter is a hint to routers and other deivces -with priority queues for network traffic that a PDU is more or less important. +of time for a response, and the latter is a hint to routers, and devices +with priority queues for network traffic, that a PDU is more or less important. -These two fields are set at the application layer and travel with the PDU -content as it travels down the stack. +These two fields are assigned at the application layer and travel with the PDU +as it travels through the stack. Encoding and Decoding --------------------- @@ -82,51 +106,80 @@ Encoding and Decoding The encoding and decoding process consists of consuming content from the source PDU and generating content in the destination. BACpypes *could* have used some kind of "visitor" pattern so the process did not consume the source, but -typically when a layer has finished with PDU and will be sending some other PDU -upstream or downstream and once that PDU leaves the layer it is not re-visited. +typically when a layer has finished with PDU it will be sending some different PDU +upstream or downstream so once the layer is finished, the PDU is not re-visited. .. note:: - This concept, where an object like a PDU is passed off to some other - function and it is no longer "owned" by the builder, is difficult to - accomplish in language and runtime environments that do not have automatic - garbage collection. It tremendiously simplifies interpreter code. + This concept, where an object like a PDU is passed off to another + function and is no longer "owned" by the builder, is difficult to + accomplish in language environments without automatic + garbage collection, but tremendiously simplifies our interpreter code. -PDUs nest the control infommation of one level into the data portion of the -next level down, and when decoding on the way up a stack it is customary to +PDUs nest the control information of one level into the data portion of the +next level. So when decoding on the way up, it is customary to pass the control information along, even when it isn't strictly necessary. The :func:`pdu.PCI.update` function is an example of a method that is used the way a "copy" operation might be used. The PCI classes, and nested versions of them, usually have an update function. -Decoding consumes some number of octets from the front of the PDU data:: +Decoding ++++++++++ + +Decoding always consumes some number of octets from the front of the PDU data. +Lets create a pdu and then use decoding to consume it:: + + >>> pdu=PDU(b'hello!!') + >>> pdu.debug_contents() + pduData = x'68.65.6c.6c.6f.21.21' + +Consume 1 octet (x'68 = decimal 104'):: - >>> pdu = PDU("hello!!") >>> pdu.get() 104 + >>> pdu.debug_contents() + pduData = x'65.6c.6c.6f.21.21' + +Consume a short integer (two octets):: + >>> pdu.get_short() 25964 + >>> pdu.debug_contents() + pduData = x'6c.6f.21.21' + +Consume a long integer (four octets):: + >>> pdu.get_long() 1819222305 + >>> pdu.debug_contents() + pduData = x'' + >>> + +And the PDU is now empty! -And the PDU is now empty:: +Encoding ++++++++++ + +We can then build the PDU contents back up through a series of *put* operations. +A *put* is an implicit append operation:: >>> pdu.debug_contents() pduData = x'' + >>> pdu.put(108) + >>> pdu.debug_contents() + pduData = x'6c' -But the contents can be put back, an implicit append operation:: - - >>> pdu.put(104) >>> pdu.put_short(25964) + >>> pdu.debug_contents() + pduData = x'6c.65.6c' + >>> pdu.put_long(1819222305) >>> pdu.debug_contents() - pduData = x'68.65.6C.6C.6F.21.21' + pduData = x'6c.65.6c.6c.6f.21.21' .. note:: - There is no distinction between a PDU that is being used as the source - to some interpretation process and one that is the destination. Earlier - versions of this library made that distinction and the type casting - and type conversion code became an impediment to understanding the - interpretation, so it was dropped. + There is no distinction between a PDU that is being taken apart (by get) + and one that is being built up (by put). + \ No newline at end of file diff --git a/doc/source/tutorial/tutorial004.rst b/doc/source/tutorial/tutorial004.rst index a27d40fd..593b6fc5 100644 --- a/doc/source/tutorial/tutorial004.rst +++ b/doc/source/tutorial/tutorial004.rst @@ -5,9 +5,18 @@ Addressing BACnet addresses come in five delicious flavors: -* local station - -* local broadcast - -* remote station - -* remote broadcast - -* global broadcast - +local station + A message addressed to one device on the same network as the originator. + +local broadcast + A message addressed to all devices or nodes on the same network as the originator. + +remote station + A message addressed to one device on a different network than the originator. + +remote broadcast + A message addressed to all devices or nodes on a different network than the originator. + +global broadcast + A message addressed to all devices or nodes on all networks known any device on any network. diff --git a/doc/source/tutorial/tutorial006.rst b/doc/source/tutorial/tutorial006.rst index 2adc6f99..7e81561d 100644 --- a/doc/source/tutorial/tutorial006.rst +++ b/doc/source/tutorial/tutorial006.rst @@ -5,20 +5,20 @@ Command Shell Debugging small, short lived BACpypes applications is fairly simple with the abillity to attach debug handlers to specific components of a stack when it -starts, thn reproducing whatever situation caused the miscreant behaviour. +starts, and then reproducing whatever situation caused the mis-behaviour. For longer running applications like gateways it might take some time before -a scenerio is ready, in which case it is advantageous to postpone debugging -output, or stop it without stopping the application. +a scenario is ready, in which case it is advantageous to start and stop the debugging +output, without stopping the application. -For some debugging scenerios it is beneficial to force some values into the +For some debugging scenarios it is beneficial to force some values into the stack, or delete some values and see how the application performs. For example, perhaps deleting a routing path associated with a network. Python has a `cmd `_ module that makes it easy to embed a command line interpreter in an application. BACpypes extends this interpreter with some commands to assist debugging and runs -the interpret in a separate thread so it does not interfere with the BACpypes +the interpreter in a separate thread so it does not interfere with the BACpypes :func:`core.run` functionality. Application Additions @@ -42,18 +42,18 @@ BACpypes applications, this can be wrapped:: Command Recall -------------- -The BACpypes command line interpreter will create a text file containing each -of the commands that were entered and load this file the next time the -application starts. Pressing the *previous command* keyboard shortcut (usually -the up-arrow key) will recall previous commands so they can be executed again. +The BACpypes command line interpreter maintains a history (text file) +of the commands executed, which it reloads upon startup. +Pressing the *previous command* keyboard shortcut (up-arrow key) +recalls previous commands so they can be executed again. Basic Commands -------------- -All of the commands are listed in the :mod:`consolecmd` documentation, but -the simplest way to learn is to try it:: +All of the commands supported are listed in the :mod:`consolecmd` documentation. +The simplest way to learn the commands is to try them:: - $ python tutorial006.py + $ python Tutorial/SampleConsoleCmd.py > hi *** Unknown syntax: hi @@ -95,71 +95,93 @@ And finally exiting the application:: Adding Commands --------------- -Adding additional commands is as simple as providing an additional function:: +Adding additional commands is as simple as providing an additional function. +Add these lines to SampleConsoleCmd.py:: - class MyConsoleCmd(ConsoleCmd): + class SampleConsoleCmd(ConsoleCmd): def do_something(self, arg): """something - do something""" - print "do something", arg + print("do something", arg) The ConsoleCmd will trap a help request ``help something`` into printing out -the documnetation string. +the documnetation string.:: + + > help + + Documented commands (type help ): + ======================================== + EOF buggers bugin bugout exit gc help nothing shell **something** + + > help something + something - do something + > + + Example Cache Commands ---------------------- -This code is in **tutorial006a.py**. The concept is to force values into an -application cache, or delete them, and dump the cache. First, setting values +Add these functions to **SampleConsoleCmd.py**. The concept is to force values into an +application cache, delete them, and dump the cache. First, setting values is a *set* command:: - def do_set(self, arg): - """set - change a cache value""" - if _debug: MyCacheCmd._debug("do_set %r", arg) - - key, value = arg.split() - my_cache[key] = value + class SampleConsoleCmd(ConsoleCmd): -Then then delete cache entries is a *del* command:: - - def do_del(self, arg): - """del - delete a cache entry""" - if _debug: MyCacheCmd._debug("do_del %r", arg) + my_cache= {} + + def do_set(self, arg): + """set - change a cache value""" + if _debug: SampleConsoleCmd._debug("do_set %r", arg) + + key, value = arg.split() + self.my_cache[key] = value + +Then delete cache entries with a *del* command:: - try: - del my_cache[arg] - except: - print arg, "not in cache" + def do_del(self, arg): + """del - delete a cache entry""" + if _debug: SampleConsoleCmd._debug("do_del %r", arg) + + try: + del self.my_cache[arg] + except: + print(arg, "not in cache") + +And to verify, dump the cache:: -And just to be sure, be able to dump the cache:: + def do_dump(self, arg): + """dump - nicely print the cache""" + if _debug: SampleConsoleCmd._debug("do_dump %r", arg) + print(self.my_cache) - def do_dump(self, arg): - """dump - nicely print the cache""" - if _debug: MyCacheCmd._debug("do_dump %r", arg) - pprint(my_cache) -And here is a sample when the application is run, note that the new commands +And when the sample application is run, note the new commands show up in the help list:: - $ python tutorial/tutorial006a.py + $ python Tutorial/SampleConsoleCmd.py > help Documented commands (type help ): ======================================== - EOF buggers bugin bugout del dump exit gc help set shell + EOF bugin **del** exit help **set** something + buggers bugout **dump** gc nothing shell + -And you can get help with a command:: +You can get help with the new commands:: > help set set - change a cache value -Add some things to the cache and dump it out:: + +Lets use these new commands to add some items to the cache and dump it out:: > set x 12 > set y 13 > dump {'x': '12', 'y': '13'} + Now add a debugger to the main application, which can generate a lot output for most applications, but this one is simple:: @@ -169,19 +191,21 @@ for most applications, but this one is simple:: Now we'll get some debug output when the cache entry is deleted:: > del x - DEBUG:__main__.MyCacheCmd:do_del 'x' + DEBUG:__main__.SampleConsoleCmd:do_del 'x' -We can see a list of buggers an which ones have a debugger attached:: +We can see a list of buggers and which ones have a debugger attached:: > buggers __main__ handlers: __main__ * __main__ - __main__.MyCacheCmd + __main__.SampleApplication + __main__.SampleConsoleCmd + Check the contents of the cache:: > dump - DEBUG:__main__.MyCacheCmd:do_dump '' + DEBUG:__main__.SampleConsoleCmd:do_dump '' {'y': '13'} All done:: diff --git a/py25/bacpypes/__init__.py b/py25/bacpypes/__init__.py index 34cd849c..d388282a 100755 --- a/py25/bacpypes/__init__.py +++ b/py25/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.14.2' +__version__ = '0.15.0' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' @@ -29,6 +29,8 @@ from . import comm from . import task from . import singleton +from . import capability +from . import iocb # # Link Layer Modules @@ -67,6 +69,7 @@ from . import app from . import appservice +from . import service # # Analysis diff --git a/py25/bacpypes/apdu.py b/py25/bacpypes/apdu.py index 4ee8c6b0..aaba304a 100755 --- a/py25/bacpypes/apdu.py +++ b/py25/bacpypes/apdu.py @@ -4,7 +4,7 @@ Application Layer Protocol Data Units """ -from .errors import DecodingError +from .errors import DecodingError, TooManyArguments from .debugging import ModuleLogger, DebugContents, bacpypes_debugging from .pdu import PCI, PDUData @@ -692,6 +692,7 @@ def decode(self, apdu): # trailing unmatched tags if self._tag_list: if _debug: APCISequence._debug(" - trailing unmatched tags") + raise TooManyArguments() def apdu_contents(self, use_dict=None, as_class=dict): """Return the contents of an object as a dict.""" diff --git a/py25/bacpypes/app.py b/py25/bacpypes/app.py index 1bcc3385..d98f2d4a 100755 --- a/py25/bacpypes/app.py +++ b/py25/bacpypes/app.py @@ -4,38 +4,34 @@ Application Module """ +import warnings + from .debugging import bacpypes_debugging, DebugContents, ModuleLogger from .comm import ApplicationServiceElement, bind +from .iocb import IOController, SieveQueue -from .pdu import Address, LocalStation, RemoteStation +from .pdu import Address -from .primitivedata import Atomic, Date, Null, ObjectIdentifier, Time, Unsigned -from .constructeddata import Any, Array, ArrayOf +from .primitivedata import ObjectIdentifier +from .capability import Collector from .appservice import StateMachineAccessPoint, ApplicationServiceAccessPoint from .netservice import NetworkServiceAccessPoint, NetworkServiceElement from .bvllservice import BIPSimple, BIPForeign, AnnexJCodec, UDPMultiplexer -from .object import Property, PropertyError, DeviceObject, \ - registered_object_types, register_object_type -from .apdu import ConfirmedRequestPDU, SimpleAckPDU, RejectPDU, RejectReason -from .apdu import IAmRequest, ReadPropertyACK, Error -from .errors import ExecutionError, \ - RejectException, UnrecognizedService, MissingRequiredParameter, \ - ParameterOutOfRange, \ - AbortException +from .apdu import UnconfirmedRequestPDU, ConfirmedRequestPDU, \ + SimpleAckPDU, ComplexAckPDU, ErrorPDU, RejectPDU, AbortPDU, Error + +from .errors import ExecutionError, UnrecognizedService, AbortException, RejectException # for computing protocol services supported from .apdu import confirmed_request_types, unconfirmed_request_types, \ ConfirmedServiceChoice, UnconfirmedServiceChoice from .basetypes import ServicesSupported -from .apdu import \ - AtomicReadFileACK, \ - AtomicReadFileACKAccessMethodChoice, \ - AtomicReadFileACKAccessMethodRecordAccess, \ - AtomicReadFileACKAccessMethodStreamAccess, \ - AtomicWriteFileACK +# basic services +from .service.device import WhoIsIAmServices +from .service.object import ReadWritePropertyServices # some debugging _debug = 0 @@ -149,7 +145,7 @@ def get_device_info(self, key): def update_device_info(self, info): """The application has updated one or more fields in the device information record and the cache needs to be updated to reflect the - changes. If this is a cached version of a persistent record then this + changes. If this is a cached version of a persistent record then this is the opportunity to update the database.""" if _debug: DeviceInfoCache._debug("update_device_info %r", info) @@ -190,145 +186,51 @@ def release_device_info(self, info): bacpypes_debugging(DeviceInfoCache) # -# CurrentDateProperty -# - -class CurrentDateProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Date, default=None, optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("%r is unsubscriptable" % (self.identifier,)) - - # get the value - now = Date() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# CurrentTimeProperty +# Application # -class CurrentTimeProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Time, default=None, optional=True, mutable=False) +class Application(ApplicationServiceElement, Collector): - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("%r is unsubscriptable" % (self.identifier,)) + def __init__(self, localDevice=None, localAddress=None, deviceInfoCache=None, aseID=None): + if _debug: Application._debug("__init__ %r %r deviceInfoCache=%r aseID=%r", localDevice, localAddress, deviceInfoCache, aseID) + ApplicationServiceElement.__init__(self, aseID) - # get the value - now = Time() - now.now() - return now.value + # local objects by ID and name + self.objectName = {} + self.objectIdentifier = {} - def WriteProperty(self, obj, value, arrayIndex=None, priority=None): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + # keep track of the local device + if localDevice: + self.localDevice = localDevice -# -# LocalDeviceObject -# + # bind the device object to this application + localDevice._app = self -class LocalDeviceObject(DeviceObject): - - properties = \ - [ CurrentTimeProperty('localTime') - , CurrentDateProperty('localDate') - ] - - defaultProperties = \ - { 'maxApduLengthAccepted': 1024 - , 'segmentationSupported': 'segmentedBoth' - , 'maxSegmentsAccepted': 16 - , 'apduSegmentTimeout': 5000 - , 'apduTimeout': 3000 - , 'numberOfApduRetries': 3 - } - - def __init__(self, **kwargs): - if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) - - # fill in default property values not in kwargs - for attr, value in LocalDeviceObject.defaultProperties.items(): - if attr not in kwargs: - kwargs[attr] = value - - # check for registration - if self.__class__ not in registered_object_types.values(): - if 'vendorIdentifier' not in kwargs: - raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") - register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) - - # check for local time - if 'localDate' in kwargs: - raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") - if 'localTime' in kwargs: - raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") - - # check for a minimum value - if kwargs['maxApduLengthAccepted'] < 50: - raise ValueError("invalid max APDU length accepted") - - # dump the updated attributes - if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) - - # proceed as usual - DeviceObject.__init__(self, **kwargs) - - # create a default implementation of an object list for local devices. - # If it is specified in the kwargs, that overrides this default. - if ('objectList' not in kwargs): - self.objectList = ArrayOf(ObjectIdentifier)([self.objectIdentifier]) - - # if the object has a property list and one wasn't provided - # in the kwargs, then it was created by default and the objectList - # property should be included - if ('propertyList' not in kwargs) and self.propertyList: - # make sure it's not already there - if 'objectList' not in self.propertyList: - self.propertyList.append('objectList') - -bacpypes_debugging(LocalDeviceObject) + # local objects by ID and name + self.objectName[localDevice.objectName] = localDevice + self.objectIdentifier[localDevice.objectIdentifier] = localDevice -# -# Application -# - -class Application(ApplicationServiceElement): - - def __init__(self, localDevice, localAddress, deviceInfoCache=None, aseID=None): - if _debug: Application._debug("__init__ %r %r deviceInfoCache=%r aseID=%r", localDevice, localAddress, deviceInfoCache, aseID) - ApplicationServiceElement.__init__(self, aseID) + # local address deprecated, but continue to use the old initializer + if localAddress is not None: + warnings.warn( + "local address at the application layer deprecated", + DeprecationWarning, + ) - # keep track of the local device - self.localDevice = localDevice + # allow the address to be cast to the correct type + if isinstance(localAddress, Address): + self.localAddress = localAddress + else: + self.localAddress = Address(localAddress) # use the provided cache or make a default one - if deviceInfoCache: - self.deviceInfoCache = deviceInfoCache - else: - self.deviceInfoCache = DeviceInfoCache() + self.deviceInfoCache = deviceInfoCache or DeviceInfoCache() - # bind the device object to this application - localDevice._app = self + # controllers for managing confirmed requests as a client + self.controllers = {} - # allow the address to be cast to the correct type - if isinstance(localAddress, Address): - self.localAddress = localAddress - else: - self.localAddress = Address(localAddress) - - # local objects by ID and name - self.objectName = {localDevice.objectName:localDevice} - self.objectIdentifier = {localDevice.objectIdentifier:localDevice} + # now set up the rest of the capabilities + Collector.__init__(self) def add_object(self, obj): """Add an object to the local collection.""" @@ -356,8 +258,10 @@ def add_object(self, obj): self.objectName[object_name] = obj self.objectIdentifier[object_identifier] = obj - # append the new object's identifier to the device's object list - self.localDevice.objectList.append(object_identifier) + # append the new object's identifier to the local device's object list + # if there is one and it has an object list property + if self.localDevice and self.localDevice.objectList: + self.localDevice.objectList.append(object_identifier) # let the object know which application stack it belongs to obj._app = self @@ -375,8 +279,10 @@ def delete_object(self, obj): del self.objectIdentifier[object_identifier] # remove the object's identifier from the device's object list - indx = self.localDevice.objectList.index(object_identifier) - del self.localDevice.objectList[indx] + # if there is one and it has an object list property + if self.localDevice and self.localDevice.objectList: + indx = self.localDevice.objectList.index(object_identifier) + del self.localDevice.objectList[indx] # make sure the object knows it's detached from an application obj._app = None @@ -419,6 +325,16 @@ def get_services_supported(self): #----- + def request(self, apdu): + if _debug: Application._debug("request %r", apdu) + + # double check the input is the right kind of APDU + if not isinstance(apdu, (UnconfirmedRequestPDU, ConfirmedRequestPDU)): + raise TypeError("APDU expected") + + # continue + super(Application, self).request(apdu) + def indication(self, apdu): if _debug: Application._debug("indication %r", apdu) @@ -458,346 +374,101 @@ def indication(self, apdu): resp = Error(errorClass='device', errorCode='operationalProblem', context=apdu) self.response(resp) - def do_WhoIsRequest(self, apdu): - """Respond to a Who-Is request.""" - if _debug: Application._debug("do_WhoIsRequest %r", apdu) - - # extract the parameters - low_limit = apdu.deviceInstanceRangeLowLimit - high_limit = apdu.deviceInstanceRangeHighLimit - - # check for consistent parameters - if (low_limit is not None): - if (high_limit is None): - raise MissingRequiredParameter("deviceInstanceRangeHighLimit required") - if (low_limit < 0) or (low_limit > 4194303): - raise ParameterOutOfRange("deviceInstanceRangeLowLimit out of range") - if (high_limit is not None): - if (low_limit is None): - raise MissingRequiredParameter("deviceInstanceRangeLowLimit required") - if (high_limit < 0) or (high_limit > 4194303): - raise ParameterOutOfRange("deviceInstanceRangeHighLimit out of range") - - # see we should respond - if (low_limit is not None): - if (self.localDevice.objectIdentifier[1] < low_limit): - return - if (high_limit is not None): - if (self.localDevice.objectIdentifier[1] > high_limit): - return - - # create a I-Am "response" back to the source - iAm = IAmRequest() - iAm.pduDestination = apdu.pduSource - iAm.iAmDeviceIdentifier = self.localDevice.objectIdentifier - iAm.maxAPDULengthAccepted = self.localDevice.maxApduLengthAccepted - iAm.segmentationSupported = self.localDevice.segmentationSupported - iAm.vendorID = self.localDevice.vendorIdentifier - if _debug: Application._debug(" - iAm: %r", iAm) +bacpypes_debugging(Application) - # away it goes - self.request(iAm) +# +# ApplicationIOController +# - def do_IAmRequest(self, apdu): - """Respond to an I-Am request.""" - if _debug: Application._debug("do_IAmRequest %r", apdu) +class ApplicationIOController(IOController, Application): - def do_ReadPropertyRequest(self, apdu): - """Return the value of some property of one of our objects.""" - if _debug: Application._debug("do_ReadPropertyRequest %r", apdu) + def __init__(self, *args, **kwargs): + if _debug: ApplicationIOController._debug("__init__") + IOController.__init__(self) + Application.__init__(self, *args, **kwargs) - # extract the object identifier - objId = apdu.objectIdentifier + # queues for each address + self.queue_by_address = {} - # check for wildcard - if (objId == ('device', 4194303)): - if _debug: Application._debug(" - wildcard device identifier") - objId = self.localDevice.objectIdentifier + def process_io(self, iocb): + if _debug: ApplicationIOController._debug("process_io %r", iocb) - # get the object - obj = self.get_object_id(objId) - if _debug: Application._debug(" - object: %r", obj) + # get the destination address from the pdu + destination_address = iocb.args[0].pduDestination + if _debug: ApplicationIOController._debug(" - destination_address: %r", destination_address) - if not obj: - resp = Error(errorClass='object', errorCode='unknownObject', context=apdu) - else: - try: - # get the datatype - datatype = obj.get_datatype(apdu.propertyIdentifier) - if _debug: Application._debug(" - datatype: %r", datatype) - - # get the value - value = obj.ReadProperty(apdu.propertyIdentifier, apdu.propertyArrayIndex) - if _debug: Application._debug(" - value: %r", value) - if value is None: - raise PropertyError(apdu.propertyIdentifier) - - # change atomic values into something encodeable - if issubclass(datatype, Atomic): - value = datatype(value) - elif issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): - if apdu.propertyArrayIndex == 0: - value = Unsigned(value) - elif issubclass(datatype.subtype, Atomic): - value = datatype.subtype(value) - elif not isinstance(value, datatype.subtype): - raise TypeError("invalid result datatype, expecting %r and got %r" \ - % (datatype.subtype.__name__, type(value).__name__)) - elif not isinstance(value, datatype): - raise TypeError("invalid result datatype, expecting %r and got %r" \ - % (datatype.__name__, type(value).__name__)) - if _debug: Application._debug(" - encodeable value: %r", value) - - # this is a ReadProperty ack - resp = ReadPropertyACK(context=apdu) - resp.objectIdentifier = objId - resp.propertyIdentifier = apdu.propertyIdentifier - resp.propertyArrayIndex = apdu.propertyArrayIndex - - # save the result in the property value - resp.propertyValue = Any() - resp.propertyValue.cast_in(value) - - except PropertyError: - resp = Error(errorClass='object', errorCode='unknownProperty', context=apdu) - if _debug: Application._debug(" - resp: %r", resp) - - # return the result - self.response(resp) - - def do_WritePropertyRequest(self, apdu): - """Change the value of some property of one of our objects.""" - if _debug: Application._debug("do_WritePropertyRequest %r", apdu) - - # get the object - obj = self.get_object_id(apdu.objectIdentifier) - if _debug: Application._debug(" - object: %r", obj) - - if not obj: - resp = Error(errorClass='object', errorCode='unknownObject', context=apdu) - else: - try: - # check if the property exists - if obj.ReadProperty(apdu.propertyIdentifier, apdu.propertyArrayIndex) is None: - raise PropertyError(apdu.propertyIdentifier) - - # get the datatype, special case for null - if apdu.propertyValue.is_application_class_null(): - datatype = Null - else: - datatype = obj.get_datatype(apdu.propertyIdentifier) - if _debug: Application._debug(" - datatype: %r", datatype) - - # special case for array parts, others are managed by cast_out - if issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): - if apdu.propertyArrayIndex == 0: - value = apdu.propertyValue.cast_out(Unsigned) - else: - value = apdu.propertyValue.cast_out(datatype.subtype) - else: - value = apdu.propertyValue.cast_out(datatype) - if _debug: Application._debug(" - value: %r", value) - - # change the value - value = obj.WriteProperty(apdu.propertyIdentifier, value, apdu.propertyArrayIndex, apdu.priority) - - # success - resp = SimpleAckPDU(context=apdu) - - except PropertyError: - resp = Error(errorClass='object', errorCode='unknownProperty', context=apdu) - if _debug: Application._debug(" - resp: %r", resp) - - # return the result - self.response(resp) - - def do_AtomicReadFileRequest(self, apdu): - """Return one of our records.""" - if _debug: Application._debug("do_AtomicReadFileRequest %r", apdu) - - if (apdu.fileIdentifier[0] != 'file'): - resp = Error(errorClass='services', errorCode='inconsistentObjectType', context=apdu) - if _debug: Application._debug(" - error resp: %r", resp) - self.response(resp) - return + # look up the queue + queue = self.queue_by_address.get(destination_address, None) + if not queue: + queue = SieveQueue(self.request, destination_address) + self.queue_by_address[destination_address] = queue + if _debug: ApplicationIOController._debug(" - queue: %r", queue) - # get the object - obj = self.get_object_id(apdu.fileIdentifier) - if _debug: Application._debug(" - object: %r", obj) - - if not obj: - resp = Error(errorClass='object', errorCode='unknownObject', context=apdu) - elif apdu.accessMethod.recordAccess: - # check against the object - if obj.fileAccessMethod != 'recordAccess': - resp = Error(errorClass='services', - errorCode='invalidFileAccessMethod', - context=apdu - ) - ### verify start is valid - double check this (empty files?) - elif (apdu.accessMethod.recordAccess.fileStartRecord < 0) or \ - (apdu.accessMethod.recordAccess.fileStartRecord >= len(obj)): - resp = Error(errorClass='services', - errorCode='invalidFileStartPosition', - context=apdu - ) - else: - # pass along to the object - end_of_file, record_data = obj.ReadFile( - apdu.accessMethod.recordAccess.fileStartRecord, - apdu.accessMethod.recordAccess.requestedRecordCount, - ) - if _debug: Application._debug(" - record_data: %r", record_data) - - # this is an ack - resp = AtomicReadFileACK(context=apdu, - endOfFile=end_of_file, - accessMethod=AtomicReadFileACKAccessMethodChoice( - recordAccess=AtomicReadFileACKAccessMethodRecordAccess( - fileStartRecord=apdu.accessMethod.recordAccess.fileStartRecord, - returnedRecordCount=len(record_data), - fileRecordData=record_data, - ), - ), - ) - - elif apdu.accessMethod.streamAccess: - # check against the object - if obj.fileAccessMethod != 'streamAccess': - resp = Error(errorClass='services', - errorCode='invalidFileAccessMethod', - context=apdu - ) - ### verify start is valid - double check this (empty files?) - elif (apdu.accessMethod.streamAccess.fileStartPosition < 0) or \ - (apdu.accessMethod.streamAccess.fileStartPosition >= len(obj)): - resp = Error(errorClass='services', - errorCode='invalidFileStartPosition', - context=apdu - ) - else: - # pass along to the object - end_of_file, record_data = obj.ReadFile( - apdu.accessMethod.streamAccess.fileStartPosition, - apdu.accessMethod.streamAccess.requestedOctetCount, - ) - if _debug: Application._debug(" - record_data: %r", record_data) - - # this is an ack - resp = AtomicReadFileACK(context=apdu, - endOfFile=end_of_file, - accessMethod=AtomicReadFileACKAccessMethodChoice( - streamAccess=AtomicReadFileACKAccessMethodStreamAccess( - fileStartPosition=apdu.accessMethod.streamAccess.fileStartPosition, - fileData=record_data, - ), - ), - ) - - if _debug: Application._debug(" - resp: %r", resp) - - # return the result - self.response(resp) - - def do_AtomicWriteFileRequest(self, apdu): - """Return one of our records.""" - if _debug: Application._debug("do_AtomicWriteFileRequest %r", apdu) - - if (apdu.fileIdentifier[0] != 'file'): - resp = Error(errorClass='services', errorCode='inconsistentObjectType', context=apdu) - if _debug: Application._debug(" - error resp: %r", resp) - self.response(resp) - return + # ask the queue to process the request + queue.request_io(iocb) - # get the object - obj = self.get_object_id(apdu.fileIdentifier) - if _debug: Application._debug(" - object: %r", obj) - - if not obj: - resp = Error(errorClass='object', errorCode='unknownObject', context=apdu) - elif apdu.accessMethod.recordAccess: - # check against the object - if obj.fileAccessMethod != 'recordAccess': - resp = Error(errorClass='services', - errorCode='invalidFileAccessMethod', - context=apdu - ) - if _debug: Application._debug(" - error resp: %r", resp) - self.response(resp) - return + def _app_complete(self, address, apdu): + if _debug: ApplicationIOController._debug("_app_complete %r %r", address, apdu) - # check for read-only - if obj.readOnly: - resp = Error(errorClass='services', - errorCode='fileAccessDenied', - context=apdu - ) - if _debug: Application._debug(" - error resp: %r", resp) - self.response(resp) - return + # look up the queue + queue = self.queue_by_address.get(address, None) + if not queue: + ApplicationIOController._debug("no queue for %r" % (address,)) + return + if _debug: ApplicationIOController._debug(" - queue: %r", queue) - # pass along to the object - start_record = obj.WriteFile( - apdu.accessMethod.recordAccess.fileStartRecord, - apdu.accessMethod.recordAccess.recordCount, - apdu.accessMethod.recordAccess.fileRecordData, - ) - if _debug: Application._debug(" - start_record: %r", start_record) + # make sure it has an active iocb + if not queue.active_iocb: + ApplicationIOController._debug("no active request for %r" % (address,)) + return - # this is an ack - resp = AtomicWriteFileACK(context=apdu, - fileStartRecord=start_record, - ) + # this request is complete + if isinstance(apdu, (None.__class__, SimpleAckPDU, ComplexAckPDU)): + queue.complete_io(queue.active_iocb, apdu) + elif isinstance(apdu, (ErrorPDU, RejectPDU, AbortPDU)): + queue.abort_io(queue.active_iocb, apdu) + else: + raise RuntimeError("unrecognized APDU type") + if _debug: Application._debug(" - controller finished") - elif apdu.accessMethod.streamAccess: - # check against the object - if obj.fileAccessMethod != 'streamAccess': - resp = Error(errorClass='services', - errorCode='invalidFileAccessMethod', - context=apdu - ) - if _debug: Application._debug(" - error resp: %r", resp) - self.response(resp) - return + # if the queue is empty and idle, forget about the controller + if not queue.ioQueue.queue and not queue.active_iocb: + if _debug: ApplicationIOController._debug(" - queue is empty") + del self.queue_by_address[address] - # check for read-only - if obj.readOnly: - resp = Error(errorClass='services', - errorCode='fileAccessDenied', - context=apdu - ) - if _debug: Application._debug(" - error resp: %r", resp) - self.response(resp) - return + def request(self, apdu): + if _debug: ApplicationIOController._debug("request %r", apdu) - # pass along to the object - start_position = obj.WriteFile( - apdu.accessMethod.streamAccess.fileStartPosition, - apdu.accessMethod.streamAccess.fileData, - ) - if _debug: Application._debug(" - start_position: %r", start_position) + # send it downstream + super(ApplicationIOController, self).request(apdu) - # this is an ack - resp = AtomicWriteFileACK(context=apdu, - fileStartPosition=start_position, - ) + # if this was an unconfirmed request, it's complete, no message + if isinstance(apdu, UnconfirmedRequestPDU): + self._app_complete(apdu.pduDestination, None) - if _debug: Application._debug(" - resp: %r", resp) + def confirmation(self, apdu): + if _debug: ApplicationIOController._debug("confirmation %r", apdu) - # return the result - self.response(resp) + # this is an ack, error, reject or abort + self._app_complete(apdu.pduSource, apdu) -bacpypes_debugging(Application) +bacpypes_debugging(ApplicationIOController) # # BIPSimpleApplication # -class BIPSimpleApplication(Application): +class BIPSimpleApplication(ApplicationIOController, WhoIsIAmServices, ReadWritePropertyServices): def __init__(self, localDevice, localAddress, deviceInfoCache=None, aseID=None): if _debug: BIPSimpleApplication._debug("__init__ %r %r deviceInfoCache=%r aseID=%r", localDevice, localAddress, deviceInfoCache, aseID) - Application.__init__(self, localDevice, localAddress, deviceInfoCache, aseID) + ApplicationIOController.__init__(self, localDevice, deviceInfoCache, aseID=aseID) + + # local address might be useful for subclasses + if isinstance(localAddress, Address): + self.localAddress = localAddress + else: + self.localAddress = Address(localAddress) # include a application decoder self.asap = ApplicationServiceAccessPoint() @@ -838,11 +509,17 @@ def __init__(self, localDevice, localAddress, deviceInfoCache=None, aseID=None): # BIPForeignApplication # -class BIPForeignApplication(Application): +class BIPForeignApplication(ApplicationIOController, WhoIsIAmServices, ReadWritePropertyServices): def __init__(self, localDevice, localAddress, bbmdAddress, bbmdTTL, aseID=None): if _debug: BIPForeignApplication._debug("__init__ %r %r %r %r aseID=%r", localDevice, localAddress, bbmdAddress, bbmdTTL, aseID) - Application.__init__(self, localDevice, localAddress, aseID) + ApplicationIOController.__init__(self, localDevice, aseID=aseID) + + # local address might be useful for subclasses + if isinstance(localAddress, Address): + self.localAddress = localAddress + else: + self.localAddress = Address(localAddress) # include a application decoder self.asap = ApplicationServiceAccessPoint() diff --git a/py25/bacpypes/appservice.py b/py25/bacpypes/appservice.py index 4a596d05..9b73c05b 100755 --- a/py25/bacpypes/appservice.py +++ b/py25/bacpypes/appservice.py @@ -1063,8 +1063,7 @@ def __init__(self, localDevice=None, deviceInfoCache=None, sap=None, cid=None): Client.__init__(self, cid) ServiceAccessPoint.__init__(self, sap) - # save a reference to the local device object and the cache - self.localDevice = localDevice + # save a reference to the device information cache self.deviceInfoCache = deviceInfoCache # client settings diff --git a/py25/bacpypes/capability.py b/py25/bacpypes/capability.py new file mode 100644 index 00000000..5e672f41 --- /dev/null +++ b/py25/bacpypes/capability.py @@ -0,0 +1,155 @@ +#!/usr/bin/python + +""" +Capability +""" + +from .debugging import bacpypes_debugging, ModuleLogger + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# Capability +# + +class Capability(object): + + _zindex = 99 + + def __init__(self): + if _debug: Capability._debug("__init__") + +bacpypes_debugging(Capability) + +# +# Collector +# + +class Collector(object): + + def __init__(self): + if _debug: Collector._debug("__init__ (%r %r)", self.__class__, self.__class__.__bases__) + + # gather the capbilities + self.capabilities = self._search_capability(self.__class__) + + # give them a chance to init + for cls in self.capabilities: + if hasattr(cls, '__init__') and cls is not Collector: + if _debug: Collector._debug(" - calling %r.__init__", cls) + cls.__init__(self) + + def _search_capability(self, base): + """Given a class, return a list of all of the derived classes that + are themselves derived from Capability.""" + if _debug: Collector._debug("_search_capability %r", base) + + rslt = [] + for cls in base.__bases__: + if issubclass(cls, Collector): + map( rslt.append, self._search_capability(cls)) + elif issubclass(cls, Capability): + rslt.append(cls) + if _debug: Collector._debug(" - rslt: %r", rslt) + + return rslt + + def capability_functions(self, fn): + """This generator yields functions that match the + requested capability sorted by z-index.""" + if _debug: Collector._debug("capability_functions %r", fn) + + # build a list of functions to call + fns = [] + for cls in self.capabilities: + xfn = getattr(cls, fn, None) + if _debug: Collector._debug(" - cls, xfn: %r, %r", cls, xfn) + if xfn: + fns.append( (getattr(cls, '_zindex', None), xfn) ) + + # sort them by z-index + fns.sort(key=lambda v: v[0]) + if _debug: Collector._debug(" - fns: %r", fns) + + # now yield them in order + for xindx, xfn in fns: + if _debug: Collector._debug(" - yield xfn: %r", xfn) + yield xfn + + def add_capability(self, cls): + """Add a capability to this object.""" + if _debug: Collector._debug("add_capability %r", cls) + + # the new type has everything the current one has plus this new one + bases = (self.__class__, cls) + if _debug: Collector._debug(" - bases: %r", bases) + + # save this additional class + self.capabilities.append(cls) + + # morph into a new type + newtype = type(self.__class__.__name__ + '+' + cls.__name__, bases, {}) + self.__class__ = newtype + + # allow the new type to init + if hasattr(cls, '__init__'): + if _debug: Collector._debug(" - calling %r.__init__", cls) + cls.__init__(self) + +bacpypes_debugging(Collector) + +# +# compose_capability +# + +def compose_capability(base, *classes): + """Create a new class starting with the base and adding capabilities.""" + if _debug: compose_capability._debug("compose_capability %r %r", base, classes) + + # make sure the base is a Collector + if not issubclass(base, Collector): + raise TypeError("base must be a subclass of Collector") + + # make sure you only add capabilities + for cls in classes: + if not issubclass(cls, Capability): + raise TypeError("%s is not a Capability subclass" % (cls,)) + + # start with everything the base has and add the new ones + bases = (base,) + classes + + # build a new name + name = base.__name__ + for cls in classes: + name += '+' + cls.__name__ + + # return a new type + return type(name, bases, {}) + +bacpypes_debugging(compose_capability) + +# +# add_capability +# + +def add_capability(base, *classes): + """Add capabilites to an existing base, all objects get the additional + functionality, but don't get inited. Use with great care!""" + if _debug: add_capability._debug("add_capability %r %r", base, classes) + + # start out with a collector + if not issubclass(base, Collector): + raise TypeError("base must be a subclass of Collector") + + # make sure you only add capabilities + for cls in classes: + if not issubclass(cls, Capability): + raise TypeError("%s is not a Capability subclass" % (cls,)) + + base.__bases__ += classes + for cls in classes: + base.__name__ += '+' + cls.__name__ + +bacpypes_debugging(add_capability) diff --git a/py25/bacpypes/comm.py b/py25/bacpypes/comm.py index b40937a3..bea40a64 100755 --- a/py25/bacpypes/comm.py +++ b/py25/bacpypes/comm.py @@ -108,12 +108,12 @@ def __init__(self, data=None, *args, **kwargs): super(PDUData, self).__init__(*args, **kwargs) # function acts like a copy constructor - if isinstance(data, PDUData) or isinstance(data, PDU): - self.pduData = _copy(data.pduData) - elif data is None: + if data is None: self.pduData = '' elif isinstance(data, str): self.pduData = data + elif isinstance(data, PDUData) or isinstance(data, PDU): + self.pduData = _copy(data.pduData) else: raise TypeError("string expected") @@ -198,7 +198,7 @@ def dict_contents(self, use_dict=None, as_class=dict): class PDU(PCI, PDUData): - def __init__(self, data='', **kwargs): + def __init__(self, data=None, **kwargs): if _debug: PDU._debug("__init__ %r %r", data, kwargs) # pick up some optional kwargs diff --git a/py25/bacpypes/console.py b/py25/bacpypes/console.py index d26d1357..3e694d93 100755 --- a/py25/bacpypes/console.py +++ b/py25/bacpypes/console.py @@ -37,7 +37,7 @@ def __init__(self, *args): class ConsoleClient(asyncore.file_dispatcher, Client): def __init__(self, cid=None): - ConsoleClient._debug("__init__ cid=%r", cid) + if _debug: ConsoleClient._debug("__init__ cid=%r", cid) asyncore.file_dispatcher.__init__(self, sys.stdin) Client.__init__(self, cid) @@ -48,13 +48,17 @@ def writable(self): return False # we don't have anything to write def handle_read(self): - deferred(ConsoleClient._debug, "handle_read") + if _debug: deferred(ConsoleClient._debug, "handle_read") + + # read from stdin (implicit encoding) data = sys.stdin.read() - deferred(ConsoleClient._debug, " - data: %r", data) - deferred(self.request, PDU(data)) + if _debug: deferred(ConsoleClient._debug, " - data: %r", data) + + # make a PDU and send it downstream + if _debug: deferred(self.request, PDU(data)) def confirmation(self, pdu): - deferred(ConsoleClient._debug, "confirmation %r", pdu) + if _debug: deferred(ConsoleClient._debug, "confirmation %r", pdu) try: sys.stdout.write(pdu.pduData) except Exception, err: @@ -69,7 +73,7 @@ def confirmation(self, pdu): class ConsoleServer(asyncore.file_dispatcher, Server): def __init__(self, sid=None): - ConsoleServer._debug("__init__ sid=%r", sid) + if _debug: ConsoleServer._debug("__init__ sid=%r", sid) asyncore.file_dispatcher.__init__(self, sys.stdin) Server.__init__(self, sid) @@ -80,16 +84,20 @@ def writable(self): return False # we don't have anything to write def handle_read(self): - deferred(ConsoleServer._debug, "handle_read") + if _debug: deferred(ConsoleServer._debug, "handle_read") + + # read from stdin (implicit encoding) data = sys.stdin.read() - deferred(ConsoleServer._debug, " - data: %r", data) - deferred(self.response, PDU(data)) + if _debug: deferred(ConsoleServer._debug, " - data: %r", data) + + # make a PDU and send it upstream + if _debug: deferred(self.response, PDU(data)) def indication(self, pdu): - deferred(ConsoleServer._debug, "Indication %r", pdu) + if _debug: deferred(ConsoleServer._debug, "indication %r", pdu) try: sys.stdout.write(pdu.pduData) except Exception, err: - ConsoleServer._exception("Indication sys.stdout.write exception: %r", err) + ConsoleServer._exception("indication sys.stdout.write exception: %r", err) bacpypes_debugging(ConsoleServer) diff --git a/py25/bacpypes/constructeddata.py b/py25/bacpypes/constructeddata.py index c3ed32b1..cea15eab 100755 --- a/py25/bacpypes/constructeddata.py +++ b/py25/bacpypes/constructeddata.py @@ -201,7 +201,7 @@ def decode(self, taglist): else: if tag.tagClass != Tag.applicationTagClass or tag.tagNumber != element.klass._app_tag: if not element.optional: - raise InvalidParameterDatatype("'%s' expected application tag %s" % (element.name, Tag._app_tag_name[element.klass._app_tag])) + raise InvalidParameterDatatype("%s expected application tag %s" % (element.name, Tag._app_tag_name[element.klass._app_tag])) else: setattr(self, element.name, None) continue @@ -291,7 +291,7 @@ def debug_contents(self, indent=1, file=sys.stdout, _ids=None): if element.optional and value is None: continue if not element.optional and value is None: - file.write("%s%s is a required element of %s\n" % (" " * indent, element.name, self.__class__.__name__)) + file.write("%s%s is a missing required element of %s\n" % (" " * indent, element.name, self.__class__.__name__)) continue if element.klass in _sequence_of_classes: diff --git a/py25/bacpypes/core.py b/py25/bacpypes/core.py index eb3d38be..098a3572 100755 --- a/py25/bacpypes/core.py +++ b/py25/bacpypes/core.py @@ -78,7 +78,7 @@ def run(spin=SPIN): # call the functions for fn, args, kwargs in fnlist: - # if _debug: run._debug(" - call: %r %r %r", fn, args, kwargs) +# if _debug: run._debug(" - call: %r %r %r", fn, args, kwargs) fn( *args, **kwargs) # done with this list @@ -214,13 +214,22 @@ def print_stack(sig, frame): # deferred # +@bacpypes_debugging def deferred(fn, *args, **kwargs): - # _log.debug("deferred %r %r %r", fn, args, kwargs) - global deferredFns +# if _debug: +# deferred._debug("deferred %r %r %r", fn, args, kwargs) +# for filename, lineno, _, _ in traceback.extract_stack()[-6:-1]: +# deferred._debug(" %s:%s" % (filename.split('/')[-1], lineno)) + global deferredFns, taskManager # append it to the list deferredFns.append((fn, args, kwargs)) + # trigger the task manager event + if taskManager and taskManager.trigger: +# if _debug: deferred._debug(" - trigger") + taskManager.trigger.set() + # # enable_sleeping # diff --git a/py25/bacpypes/errors.py b/py25/bacpypes/errors.py index 7c1dcba5..bb25871b 100755 --- a/py25/bacpypes/errors.py +++ b/py25/bacpypes/errors.py @@ -100,7 +100,7 @@ class InconsistentParameters(RejectException): conditional service argument that should not be present. This condition could also elicit a Reject PDU with a Reject Reason of INVALID_TAG. """ - + rejectReason = 'inconsistentParameters' diff --git a/py25/bacpypes/iocb.py b/py25/bacpypes/iocb.py new file mode 100644 index 00000000..3595620a --- /dev/null +++ b/py25/bacpypes/iocb.py @@ -0,0 +1,1014 @@ +#!/usr/bin/python + +""" +IOCB Module +""" + +import sys +import logging +from time import time as _time + +import threading +from bisect import bisect_left + +from .debugging import bacpypes_debugging, ModuleLogger, DebugContents + +from .core import deferred +from .task import FunctionTask +from .comm import Client + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) +_statelog = logging.getLogger(__name__ + "._statelog") + +# globals +local_controllers = {} + +# +# IOCB States +# + +IDLE = 0 # has not been submitted +PENDING = 1 # queued, waiting for processing +ACTIVE = 2 # being processed +COMPLETED = 3 # finished +ABORTED = 4 # finished in a bad way + +_stateNames = { + 0: 'IDLE', + 1: 'PENDING', + 2: 'ACTIVE', + 3: 'COMPLETED', + 4: 'ABORTED', + } + +# +# IOQController States +# + +CTRL_IDLE = 0 # nothing happening +CTRL_ACTIVE = 1 # working on an iocb +CTRL_WAITING = 1 # waiting between iocb requests (throttled) + +_ctrlStateNames = { + 0: 'IDLE', + 1: 'ACTIVE', + 2: 'WAITING', + } + +# special abort error +TimeoutError = RuntimeError("timeout") + +# current time formatting (short version) +_strftime = lambda: "%011.6f" % (_time() % 3600,) + +# +# IOCB - Input Output Control Block +# + +_identNext = 1 +_identLock = threading.Lock() + +class IOCB(DebugContents): + + _debugContents = \ + ( 'args', 'kwargs' + , 'ioState', 'ioResponse-', 'ioError' + , 'ioController', 'ioServerRef', 'ioControllerRef', 'ioClientID', 'ioClientAddr' + , 'ioComplete', 'ioCallback+', 'ioQueue', 'ioPriority', 'ioTimeout' + ) + + def __init__(self, *args, **kwargs): + global _identNext + + # lock the identity sequence number + _identLock.acquire() + + # generate a unique identity for this block + ioID = _identNext + _identNext += 1 + + # release the lock + _identLock.release() + + # debugging postponed until ID acquired + if _debug: IOCB._debug("__init__(%d) %r %r", ioID, args, kwargs) + + # save the ID + self.ioID = ioID + + # save the request parameters + self.args = args + self.kwargs = kwargs + + # start with an idle request + self.ioState = IDLE + self.ioResponse = None + self.ioError = None + + # blocks are bound to a controller + self.ioController = None + + # each block gets a completion event + self.ioComplete = threading.Event() + self.ioComplete.clear() + + # applications can set a callback functions + self.ioCallback = [] + + # request is not currently queued + self.ioQueue = None + + # extract the priority if it was given + self.ioPriority = kwargs.get('_priority', 0) + if '_priority' in kwargs: + if _debug: IOCB._debug(" - ioPriority: %r", self.ioPriority) + del kwargs['_priority'] + + # request has no timeout + self.ioTimeout = None + + def add_callback(self, fn, *args, **kwargs): + """Pass a function to be called when IO is complete.""" + if _debug: IOCB._debug("add_callback(%d) %r %r %r", self.ioID, fn, args, kwargs) + + # store it + self.ioCallback.append((fn, args, kwargs)) + + # already complete? + if self.ioComplete.isSet(): + self.trigger() + + def wait(self, *args): + """Wait for the completion event to be set.""" + if _debug: IOCB._debug("wait(%d) %r", self.ioID, args) + + # waiting from a non-daemon thread could be trouble + self.ioComplete.wait(*args) + + def trigger(self): + """Set the completion event and make the callback(s).""" + if _debug: IOCB._debug("trigger(%d)", self.ioID) + + # if it's queued, remove it from its queue + if self.ioQueue: + if _debug: IOCB._debug(" - dequeue") + self.ioQueue.Remove(self) + + # if there's a timer, cancel it + if self.ioTimeout: + if _debug: IOCB._debug(" - cancel timeout") + self.ioTimeout.SuspendTask() + + # set the completion event + self.ioComplete.set() + + # make the callback(s) + for fn, args, kwargs in self.ioCallback: + if _debug: IOCB._debug(" - callback fn: %r %r %r", fn, args, kwargs) + fn(self, *args, **kwargs) + + def complete(self, msg): + """Called to complete a transaction, usually when ProcessIO has + shipped the IOCB off to some other thread or function.""" + if _debug: IOCB._debug("complete(%d) %r", self.ioID, msg) + + if self.ioController: + # pass to controller + self.ioController.complete_io(self, msg) + else: + # just fill in the data + self.ioState = COMPLETED + self.ioResponse = msg + self.trigger() + + def abort(self, err): + """Called by a client to abort a transaction.""" + if _debug: IOCB._debug("abort(%d) %r", self.ioID, err) + + if self.ioController: + # pass to controller + self.ioController.abort_io(self, err) + elif self.ioState < COMPLETED: + # just fill in the data + self.ioState = ABORTED + self.ioError = err + self.trigger() + + def set_timeout(self, delay, err=TimeoutError): + """Called to set a transaction timer.""" + if _debug: IOCB._debug("set_timeout(%d) %r err=%r", self.ioID, delay, err) + + # if one has already been created, cancel it + if self.ioTimeout: + self.ioTimeout.suspend_task() + else: + self.ioTimeout = FunctionTask(self.Abort, err) + + # (re)schedule it + self.ioTimeout.install_task(_time() + delay) + + def __repr__(self): + xid = id(self) + if (xid < 0): xid += (1 << 32) + + sname = self.__module__ + '.' + self.__class__.__name__ + desc = "(%d)" % (self.ioID) + + return '<' + sname + desc + ' instance at 0x%08x' % (xid,) + '>' + +bacpypes_debugging(IOCB) + +# +# IOChainMixIn +# + +class IOChainMixIn(DebugContents): + + _debugContents = ( 'ioChain++', ) + + def __init__(self, iocb): + if _debug: IOChainMixIn._debug("__init__ %r", iocb) + + # save a refence back to the iocb + self.ioChain = iocb + + # set the callback to follow the chain + self.add_callback(self.chain_callback) + + # if we're not chained, there's no notification to do + if not self.ioChain: + return + + # this object becomes its controller + iocb.ioController = self + + # consider the parent active + iocb.ioState = ACTIVE + + try: + if _debug: IOChainMixIn._debug(" - encoding") + + # let the derived class set the args and kwargs + self.encode() + + if _debug: IOChainMixIn._debug(" - encode complete") + except: + # extract the error and abort the request + err = sys.exc_info()[1] + if _debug: IOChainMixIn._exception(" - encoding exception: %r", err) + + iocb.abort(err) + + def chain_callback(self, iocb): + """Callback when this iocb completes.""" + if _debug: IOChainMixIn._debug("chain_callback %r", iocb) + + # if we're not chained, there's no notification to do + if not self.ioChain: + return + + # refer to the chained iocb + iocb = self.ioChain + + try: + if _debug: IOChainMixIn._debug(" - decoding") + + # let the derived class transform the data + self.decode() + + if _debug: IOChainMixIn._debug(" - decode complete") + except: + # extract the error and abort + err = sys.exc_info()[1] + if _debug: IOChainMixIn._exception(" - decoding exception: %r", err) + + iocb.ioState = ABORTED + iocb.ioError = err + + # break the references + self.ioChain = None + iocb.ioController = None + + # notify the client + iocb.trigger() + + def abort_io(self, iocb, err): + """Forward the abort downstream.""" + if _debug: IOChainMixIn._debug("abort_io %r %r", iocb, err) + + # make sure we're being notified of an abort request from + # the iocb we are chained from + if iocb is not self.ioChain: + raise RuntimeError("broken chain") + + # call my own Abort(), which may forward it to a controller or + # be overridden by IOGroup + self.abort(err) + + def encode(self): + """Hook to transform the request, called when this IOCB is + chained.""" + if _debug: IOChainMixIn._debug("encode") + + # by default do nothing, the arguments have already been supplied + + def decode(self): + """Hook to transform the response, called when this IOCB is + completed.""" + if _debug: IOChainMixIn._debug("decode") + + # refer to the chained iocb + iocb = self.ioChain + + # if this has completed successfully, pass it up + if self.ioState == COMPLETED: + if _debug: IOChainMixIn._debug(" - completed: %r", self.ioResponse) + + # change the state and transform the content + iocb.ioState = COMPLETED + iocb.ioResponse = self.ioResponse + + # if this aborted, pass that up too + elif self.ioState == ABORTED: + if _debug: IOChainMixIn._debug(" - aborted: %r", self.ioError) + + # change the state + iocb.ioState = ABORTED + iocb.ioError = self.ioError + + else: + raise RuntimeError("invalid state: %d" % (self.ioState,)) + +bacpypes_debugging(IOChainMixIn) + +# +# IOChain +# + +class IOChain(IOCB, IOChainMixIn): + + def __init__(self, chain, *args, **kwargs): + """Initialize a chained control block.""" + if _debug: IOChain._debug("__init__ %r %r %r", chain, args, kwargs) + + # initialize IOCB part to pick up the ioID + IOCB.__init__(self, *args, **kwargs) + IOChainMixIn.__init__(self, chain) + +bacpypes_debugging(IOChain) + +# +# IOGroup +# + +class IOGroup(IOCB, DebugContents): + + _debugContents = ('ioMembers',) + + def __init__(self): + """Initialize a group.""" + if _debug: IOGroup._debug("__init__") + IOCB.__init__(self) + + # start with an empty list of members + self.ioMembers = [] + + # start out being done. When an IOCB is added to the + # group that is not already completed, this state will + # change to PENDING. + self.ioState = COMPLETED + self.ioComplete.set() + + def add(self, iocb): + """Add an IOCB to the group, you can also add other groups.""" + if _debug: IOGroup._debug("add %r", iocb) + + # add this to our members + self.ioMembers.append(iocb) + + # assume all of our members have not completed yet + self.ioState = PENDING + self.ioComplete.clear() + + # when this completes, call back to the group. If this + # has already completed, it will trigger + iocb.add_callback(self.group_callback) + + def group_callback(self, iocb): + """Callback when a child iocb completes.""" + if _debug: IOGroup._debug("group_callback %r", iocb) + + # check all the members + for iocb in self.ioMembers: + if not iocb.ioComplete.isSet(): + if _debug: IOGroup._debug(" - waiting for child: %r", iocb) + break + else: + if _debug: IOGroup._debug(" - all children complete") + # everything complete + self.ioState = COMPLETED + self.trigger() + + def abort(self, err): + """Called by a client to abort all of the member transactions. + When the last pending member is aborted the group callback + function will be called.""" + if _debug: IOGroup._debug("abort %r", err) + + # change the state to reflect that it was killed + self.ioState = ABORTED + self.ioError = err + + # abort all the members + for iocb in self.ioMembers: + iocb.abort(err) + + # notify the client + self.trigger() + +bacpypes_debugging(IOGroup) + +# +# IOQueue +# + +class IOQueue: + + def __init__(self, name=None): + if _debug: IOQueue._debug("__init__ %r", name) + + self.notempty = threading.Event() + self.notempty.clear() + + self.queue = [] + + def put(self, iocb): + """Add an IOCB to a queue. This is usually called by the function + that filters requests and passes them out to the correct processing + thread.""" + if _debug: IOQueue._debug("put %r", iocb) + + # requests should be pending before being queued + if iocb.ioState != PENDING: + raise RuntimeError("invalid state transition") + + # save that it might have been empty + wasempty = not self.notempty.isSet() + + # add the request to the end of the list of iocb's at same priority + priority = iocb.ioPriority + item = (priority, iocb) + self.queue.insert(bisect_left(self.queue, (priority+1,)), item) + + # point the iocb back to this queue + iocb.ioQueue = self + + # set the event, queue is no longer empty + self.notempty.set() + + return wasempty + + def get(self, block=1, delay=None): + """Get a request from a queue, optionally block until a request + is available.""" + if _debug: IOQueue._debug("get block=%r delay=%r", block, delay) + + # if the queue is empty and we do not block return None + if not block and not self.notempty.isSet(): + return None + + # wait for something to be in the queue + if delay: + self.notempty.wait(delay) + if not self.notempty.isSet(): + return None + else: + self.notempty.wait() + + # extract the first element + priority, iocb = self.queue[0] + del self.queue[0] + iocb.ioQueue = None + + # if the queue is empty, clear the event + qlen = len(self.queue) + if not qlen: + self.notempty.clear() + + # return the request + return iocb + + def remove(self, iocb): + """Remove a control block from the queue, called if the request + is canceled/aborted.""" + if _debug: IOQueue._debug("remove %r", iocb) + + # remove the request from the queue + for i, item in enumerate(self.queue): + if iocb is item[1]: + if _debug: IOQueue._debug(" - found at %d", i) + del self.queue[i] + + # if the queue is empty, clear the event + qlen = len(self.queue) + if not qlen: + self.notempty.clear() + + # record the new length + # self.queuesize.Record( qlen, _time() ) + break + else: + if _debug: IOQueue._debug(" - not found") + + def abort(self, err): + """Abort all of the control blocks in the queue.""" + if _debug: IOQueue._debug("abort %r", err) + + # send aborts to all of the members + try: + for iocb in self.queue: + iocb.ioQueue = None + iocb.abort(err) + + # flush the queue + self.queue = [] + + # the queue is now empty, clear the event + self.notempty.clear() + except ValueError: + pass + +bacpypes_debugging(IOQueue) + +# +# IOController +# + +class IOController(object): + + def __init__(self, name=None): + """Initialize a controller.""" + if _debug: IOController._debug("__init__ name=%r", name) + + # save the name + self.name = name + + def abort(self, err): + """Abort all requests, no default implementation.""" + pass + + def request_io(self, iocb): + """Called by a client to start processing a request.""" + if _debug: IOController._debug("request_io %r", iocb) + + # check that the parameter is an IOCB + if not isinstance(iocb, IOCB): + raise TypeError("IOCB expected") + + # bind the iocb to this controller + iocb.ioController = self + + try: + # hopefully there won't be an error + err = None + + # change the state + iocb.ioState = PENDING + + # let derived class figure out how to process this + self.process_io(iocb) + except: + # extract the error + err = sys.exc_info()[1] + + # if there was an error, abort the request + if err: + self.abort_io(iocb, err) + + def process_io(self, iocb): + """Figure out how to respond to this request. This must be + provided by the derived class.""" + raise NotImplementedError("IOController must implement process_io()") + + def active_io(self, iocb): + """Called by a handler to notify the controller that a request is + being processed.""" + if _debug: IOController._debug("active_io %r", iocb) + + # requests should be idle or pending before coming active + if (iocb.ioState != IDLE) and (iocb.ioState != PENDING): + raise RuntimeError("invalid state transition (currently %d)" % (iocb.ioState,)) + + # change the state + iocb.ioState = ACTIVE + + def complete_io(self, iocb, msg): + """Called by a handler to return data to the client.""" + if _debug: IOController._debug("complete_io %r %r", iocb, msg) + + # if it completed, leave it alone + if iocb.ioState == COMPLETED: + pass + + # if it already aborted, leave it alone + elif iocb.ioState == ABORTED: + pass + + else: + # change the state + iocb.ioState = COMPLETED + iocb.ioResponse = msg + + # notify the client + iocb.trigger() + + def abort_io(self, iocb, err): + """Called by a handler or a client to abort a transaction.""" + if _debug: IOController._debug("abort_io %r %r", iocb, err) + + # if it completed, leave it alone + if iocb.ioState == COMPLETED: + pass + + # if it already aborted, leave it alone + elif iocb.ioState == ABORTED: + pass + + else: + # change the state + iocb.ioState = ABORTED + iocb.ioError = err + + # notify the client + iocb.trigger() + +bacpypes_debugging(IOController) + +# +# IOQController +# + +class IOQController(IOController): + + wait_time = 0.0 + + def __init__(self, name=None): + """Initialize a queue controller.""" + if _debug: IOQController._debug("__init__ name=%r", name) + IOController.__init__(self, name) + + # start idle + self.state = CTRL_IDLE + _statelog.debug("%s %s %s" % (_strftime(), self.name, "idle")) + + # no active iocb + self.active_iocb = None + + # create an IOQueue for iocb's requested when not idle + self.ioQueue = IOQueue(str(name) + " queue") + + def abort(self, err): + """Abort all pending requests.""" + if _debug: IOQController._debug("abort %r", err) + + if (self.state == CTRL_IDLE): + if _debug: IOQController._debug(" - idle") + return + + while True: + iocb = self.ioQueue.get() + if not iocb: + break + if _debug: IOQController._debug(" - iocb: %r", iocb) + + # change the state + iocb.ioState = ABORTED + iocb.ioError = err + + # notify the client + iocb.trigger() + + if (self.state != CTRL_IDLE): + if _debug: IOQController._debug(" - busy after aborts") + + def request_io(self, iocb): + """Called by a client to start processing a request.""" + if _debug: IOQController._debug("request_io %r", iocb) + + # bind the iocb to this controller + iocb.ioController = self + + # if we're busy, queue it + if (self.state != CTRL_IDLE): + if _debug: IOQController._debug(" - busy, request queued") + + iocb.ioState = PENDING + self.ioQueue.put(iocb) + return + + try: + # hopefully there won't be an error + err = None + + # let derived class figure out how to process this + self.process_io(iocb) + except: + # extract the error + err = sys.exc_info()[1] + + # if there was an error, abort the request + if err: + self.abort_io(iocb, err) + + def process_io(self, iocb): + """Figure out how to respond to this request. This must be + provided by the derived class.""" + raise NotImplementedError("IOController must implement process_io()") + + def active_io(self, iocb): + """Called by a handler to notify the controller that a request is + being processed.""" + if _debug: IOQController._debug("active_io %r", iocb) + + # base class work first, setting iocb state and timer data + IOController.active_io(self, iocb) + + # change our state + self.state = CTRL_ACTIVE + _statelog.debug("%s %s %s" % (_strftime(), self.name, "active")) + + # keep track of the iocb + self.active_iocb = iocb + + def complete_io(self, iocb, msg): + """Called by a handler to return data to the client.""" + if _debug: IOQController._debug("complete_io %r %r", iocb, msg) + + # check to see if it is completing the active one + if iocb is not self.active_iocb: + raise RuntimeError("not the current iocb") + + # normal completion + IOController.complete_io(self, iocb, msg) + + # no longer an active iocb + self.active_iocb = None + + # check to see if we should wait a bit + if self.wait_time: + # change our state + self.state = CTRL_WAITING + _statelog.debug("%s %s %s" % (_strftime(), self.name, "waiting")) + + # schedule a call in the future + task = FunctionTask(IOQController._wait_trigger, self) + task.install_task(_time() + self.wait_time) + + else: + # change our state + self.state = CTRL_IDLE + _statelog.debug("%s %s %s" % (_strftime(), self.name, "idle")) + + # look for more to do + deferred(IOQController._trigger, self) + + def abort_io(self, iocb, err): + """Called by a handler or a client to abort a transaction.""" + if _debug: IOQController._debug("abort_io %r %r", iocb, err) + + # normal abort + IOController.abort_io(self, iocb, err) + + # check to see if it is completing the active one + if iocb is not self.active_iocb: + if _debug: IOQController._debug(" - not current iocb") + return + + # no longer an active iocb + self.active_iocb = None + + # change our state + self.state = CTRL_IDLE + _statelog.debug("%s %s %s" % (_strftime(), self.name, "idle")) + + # look for more to do + deferred(IOQController._trigger, self) + + def _trigger(self): + """Called to launch the next request in the queue.""" + if _debug: IOQController._debug("_trigger") + + # if we are busy, do nothing + if self.state != CTRL_IDLE: + if _debug: IOQController._debug(" - not idle") + return + + # if there is nothing to do, return + if not self.ioQueue.queue: + if _debug: IOQController._debug(" - empty queue") + return + + # get the next iocb + iocb = self.ioQueue.get() + + try: + # hopefully there won't be an error + err = None + + # let derived class figure out how to process this + self.process_io(iocb) + except: + # extract the error + err = sys.exc_info()[1] + + # if there was an error, abort the request + if err: + self.abort_io(iocb, err) + + # if we're idle, call again + if self.state == CTRL_IDLE: + deferred(IOQController._trigger, self) + + def _wait_trigger(self): + """Called to launch the next request in the queue.""" + if _debug: IOQController._debug("_wait_trigger") + + # make sure we are waiting + if (self.state != CTRL_WAITING): + raise RuntimeError("not waiting") + + # change our state + self.state = CTRL_IDLE + _statelog.debug("%s %s %s" % (_strftime(), self.name, "idle")) + + # look for more to do + IOQController._trigger(self) + +bacpypes_debugging(IOQController) + +# +# ClientController +# + +class ClientController(Client, IOQController): + + def __init__(self): + if _debug: ClientController._debug("__init__") + Client.__init__(self) + IOQController.__init__(self) + + def process_io(self, iocb): + if _debug: ClientController._debug("process_io %r", iocb) + + # this is now an active request + self.active_io(iocb) + + # send the PDU downstream + self.request(iocb.args[0]) + + def confirmation(self, pdu): + if _debug: ClientController._debug("confirmation %r %r", args, kwargs) + + # make sure it has an active iocb + if not self.active_iocb: + ClientController._debug("no active request") + return + + # look for exceptions + if isinstance(pdu, Exception): + self.abort_io(self.active_iocb, pdu) + else: + self.complete_io(self.active_iocb, pdu) + +bacpypes_debugging(ClientController) + +# +# SieveQueue +# + +class SieveQueue(IOQController): + + def __init__(self, request_fn, address=None): + if _debug: SieveQueue._debug("__init__ %r %r", request_fn, address) + IOQController.__init__(self, str(address)) + + # save a reference to the request function + self.request_fn = request_fn + self.address = address + + def process_io(self, iocb): + if _debug: SieveQueue._debug("process_io %r", iocb) + + # this is now an active request + self.active_io(iocb) + + # send the request + self.request_fn(iocb.args[0]) + +bacpypes_debugging(SieveQueue) + +# +# SieveClientController +# + +class SieveClientController(Client, IOController): + + def __init__(self): + if _debug: SieveClientController._debug("__init__") + Client.__init__(self) + IOController.__init__(self) + + # queues for each address + self.queues = {} + + def process_io(self, iocb): + if _debug: SieveClientController._debug("process_io %r", iocb) + + # get the destination address from the pdu + destination_address = iocb.args[0].pduDestination + if _debug: SieveClientController._debug(" - destination_address: %r", destination_address) + + # look up the queue + queue = self.queues.get(destination_address, None) + if not queue: + queue = SieveQueue(self.request, destination_address) + self.queues[destination_address] = queue + if _debug: SieveClientController._debug(" - queue: %r", queue) + + # ask the queue to process the request + queue.request_io(iocb) + + def request(self, pdu): + if _debug: SieveClientController._debug("request %r", pdu) + + # send it downstream + super(SieveClientController, self).request(pdu) + + def confirmation(self, pdu): + if _debug: SieveClientController._debug("confirmation %r", pdu) + + # get the source address + source_address = pdu.pduSource + if _debug: SieveClientController._debug(" - source_address: %r", source_address) + + # look up the queue + queue = self.queues.get(source_address, None) + if not queue: + SieveClientController._debug("no queue for %r" % (source_address,)) + return + if _debug: SieveClientController._debug(" - queue: %r", queue) + + # make sure it has an active iocb + if not queue.active_iocb: + SieveClientController._debug("no active request for %r" % (source_address,)) + return + + # complete the request + if isinstance(pdu, Exception): + queue.abort_io(queue.active_iocb, pdu) + else: + queue.complete_io(queue.active_iocb, pdu) + + # if the queue is empty and idle, forget about the controller + if not queue.ioQueue.queue and not queue.active_iocb: + if _debug: SieveClientController._debug(" - queue is empty") + del self.queues[source_address] + +bacpypes_debugging(SieveClientController) + +# +# register_controller +# + +def register_controller(controller): + if _debug: register_controller._debug("register_controller %r", controller) + global local_controllers + + # skip those that shall not be named + if not controller.name: + return + + # make sure there isn't one already + if controller.name in local_controllers: + raise RuntimeError("already a local controller named %r" % (controller.name,)) + + local_controllers[controller.name] = controller + +bacpypes_debugging(register_controller) + +# +# abort +# + +def abort(err): + """Abort everything, everywhere.""" + if _debug: abort._debug("abort %r", err) + global local_controllers + + # tell all the local controllers to abort + for controller in local_controllers.values(): + controller.abort(err) + +bacpypes_debugging(abort) diff --git a/py25/bacpypes/object.py b/py25/bacpypes/object.py index eaa5a82b..170cdb45 100755 --- a/py25/bacpypes/object.py +++ b/py25/bacpypes/object.py @@ -5,6 +5,8 @@ """ import sys +from copy import copy as _copy +from collections import defaultdict from .errors import ConfigurationError, ExecutionError, \ InvalidParameterDatatype @@ -172,8 +174,11 @@ def ReadProperty(self, obj, arrayIndex=None): raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') if value is not None: - # dive in, the water's fine - value = value[arrayIndex] + try: + # dive in, the water's fine + value = value[arrayIndex] + except IndexError: + raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') # all set return value @@ -209,6 +214,9 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False self.identifier, self.datatype.__name__, )) + # local check if the property is monitored + is_monitored = self.identifier in obj._property_monitors + if arrayIndex is not None: if not issubclass(self.datatype, Array): raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') @@ -218,14 +226,34 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False if arry is None: raise RuntimeError("%s uninitialized array" % (self.identifier,)) + if is_monitored: + old_value = _copy(arry) + # seems to be OK, let the array object take over if _debug: Property._debug(" - forwarding to array") - arry[arrayIndex] = value + try: + arry[arrayIndex] = value + except IndexError: + raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') - return + # check for monitors, call each one with the old and new value + if is_monitored: + for fn in obj._property_monitors[self.identifier]: + if _debug: Property._debug(" - monitor: %r", fn) + fn(old_value, arry) + + else: + if is_monitored: + old_value = obj._values.get(self.identifier, None) - # seems to be OK - obj._values[self.identifier] = value + # seems to be OK + obj._values[self.identifier] = value + + # check for monitors, call each one with the old and new value + if is_monitored: + for fn in obj._property_monitors[self.identifier]: + if _debug: Property._debug(" - monitor: %r", fn) + fn(old_value, value) # # StandardProperty @@ -358,6 +386,9 @@ def __init__(self, **kwargs): # start with a clean dict of values self._values = {} + # empty list of property monitors + self._property_monitors = defaultdict(list) + # start with a clean array of property identifiers if 'propertyList' in initargs: propertyList = None @@ -433,6 +464,49 @@ def __setattr__(self, attr, value): return prop.WriteProperty(self, value, direct=True) + def add_property(self, prop): + """Add a property to an object. The property is an instance of + a Property or one of its derived classes. Adding a property + disconnects it from the collection of properties common to all of the + objects of its class.""" + if _debug: Object._debug("add_property %r", prop) + + # make a copy of the properties dictionary + self._properties = _copy(self._properties) + + # save the property reference and default value (usually None) + self._properties[prop.identifier] = prop + self._values[prop.identifier] = prop.default + + # tell the object it has a new property + if 'propertyList' in self._values: + property_list = self.propertyList + if prop.identifier not in property_list: + if _debug: Object._debug(" - adding to property list") + property_list.append(prop.identifier) + + def delete_property(self, prop): + """Delete a property from an object. The property is an instance of + a Property or one of its derived classes, but only the property + is relavent. Deleting a property disconnects it from the collection of + properties common to all of the objects of its class.""" + if _debug: Object._debug("delete_property %r", value) + + # make a copy of the properties dictionary + self._properties = _copy(self._properties) + + # delete the property from the dictionary and values + del self._properties[prop.identifier] + if prop.identifier in self._values: + del self._values[prop.identifier] + + # remove the property identifier from its list of know properties + if 'propertyList' in self._values: + property_list = self.propertyList + if prop.identifier in property_list: + if _debug: Object._debug(" - removing from property list") + property_list.remove(prop.identifier) + def ReadProperty(self, propid, arrayIndex=None): if _debug: Object._debug("ReadProperty %r arrayIndex=%r", propid, arrayIndex) @@ -522,13 +596,14 @@ def debug_contents(self, indent=1, file=sys.stdout, _ids=None): file.write("%s%s = %s\n" % (" " * indent, attr, getattr(self, attr))) previous_attrs = attrs - # build a list of properties "bottom up" + # build a list of property identifiers "bottom up" property_names = [] + properties_seen = set() for c in klasses: - properties = getattr(c, 'properties', []) - for property in properties: - if property.identifier not in property_names: - property_names.append(property.identifier) + for prop in getattr(c, 'properties', []): + if prop.identifier not in properties_seen: + property_names.append(prop.identifier) + properties_seen.add(prop.identifier) # print out the values for property_name in property_names: @@ -1282,6 +1357,7 @@ class DeviceObject(Object): , OptionalProperty('timeSynchronizationInterval', Unsigned) , OptionalProperty('alignIntervals', Boolean) , OptionalProperty('intervalOffset', Unsigned) + , OptionalProperty('serialNumber', CharacterString) ] register_object_type(DeviceObject) diff --git a/py25/bacpypes/primitivedata.py b/py25/bacpypes/primitivedata.py index 823f26a5..363672f6 100755 --- a/py25/bacpypes/primitivedata.py +++ b/py25/bacpypes/primitivedata.py @@ -1239,7 +1239,7 @@ class Date(Atomic): def __init__(self, arg=None, year=255, month=255, day=255, day_of_week=255): self.value = (year, month, day, day_of_week) - + if arg is None: pass elif isinstance(arg, Tag): @@ -1357,7 +1357,7 @@ def CalcDayOfWeek(self): elif day in _special_day_inv: pass else: - try: + try: today = time.mktime( (year + 1900, month, day, 0, 0, 0, 0, 0, -1) ) day_of_week = time.gmtime(today)[6] + 1 except OverflowError: diff --git a/py25/bacpypes/service/__init__.py b/py25/bacpypes/service/__init__.py new file mode 100644 index 00000000..69329988 --- /dev/null +++ b/py25/bacpypes/service/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python + +""" +Service Subpackage +""" + +from . import test +from . import detect + +from . import device +from . import object +from . import cov +from . import file diff --git a/py25/bacpypes/service/cov.py b/py25/bacpypes/service/cov.py new file mode 100644 index 00000000..69b8dd38 --- /dev/null +++ b/py25/bacpypes/service/cov.py @@ -0,0 +1,645 @@ +#!/usr/bin/env python + +""" +Change Of Value Service +""" + +from ..debugging import bacpypes_debugging, DebugContents, ModuleLogger +from ..capability import Capability + +from ..task import OneShotTask, TaskManager +from ..iocb import IOCB + +from ..basetypes import DeviceAddress, COVSubscription, PropertyValue, \ + Recipient, RecipientProcess, ObjectPropertyReference +from ..constructeddata import SequenceOf, Any +from ..apdu import ConfirmedCOVNotificationRequest, \ + UnconfirmedCOVNotificationRequest, \ + SimpleAckPDU, Error, RejectPDU, AbortPDU +from ..errors import ExecutionError + +from ..object import Property +from .detect import DetectionAlgorithm, monitor_filter + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# SubscriptionList +# + +class SubscriptionList: + + def __init__(self): + if _debug: SubscriptionList._debug("__init__") + + self.cov_subscriptions = [] + + def append(self, cov): + if _debug: SubscriptionList._debug("append %r", cov) + + self.cov_subscriptions.append(cov) + + def remove(self, cov): + if _debug: SubscriptionList._debug("remove %r", cov) + + self.cov_subscriptions.remove(cov) + + def find(self, client_addr, proc_id, obj_id): + if _debug: SubscriptionList._debug("find %r %r %r", client_addr, proc_id, obj_id) + + for cov in self.cov_subscriptions: + all_equal = (cov.client_addr == client_addr) and \ + (cov.proc_id == proc_id) and \ + (cov.obj_id == obj_id) + if _debug: SubscriptionList._debug(" - cov, all_equal: %r %r", cov, all_equal) + + if all_equal: + return cov + + return None + + def __len__(self): + if _debug: SubscriptionList._debug("__len__") + + return len(self.cov_subscriptions) + + def __iter__(self): + if _debug: SubscriptionList._debug("__iter__") + + for cov in self.cov_subscriptions: + yield cov + +bacpypes_debugging(SubscriptionList) + +# +# Subscription +# + +class Subscription(OneShotTask, DebugContents): + + _debug_contents = ( + 'obj_ref', + 'client_addr', + 'proc_id', + 'obj_id', + 'confirmed', + 'lifetime', + ) + + def __init__(self, obj_ref, client_addr, proc_id, obj_id, confirmed, lifetime): + if _debug: Subscription._debug("__init__ %r %r %r %r %r %r", obj_ref, client_addr, proc_id, obj_id, confirmed, lifetime) + OneShotTask.__init__(self) + + # save the reference to the related object + self.obj_ref = obj_ref + + # save the parameters + self.client_addr = client_addr + self.proc_id = proc_id + self.obj_id = obj_id + self.confirmed = confirmed + self.lifetime = lifetime + + # if lifetime is non-zero, schedule the subscription to expire + if lifetime != 0: + self.install_task(delta=self.lifetime) + + def cancel_subscription(self): + if _debug: Subscription._debug("cancel_subscription") + + # suspend the task + self.suspend_task() + + # tell the application to cancel us + self.obj_ref._app.cancel_subscription(self) + + # break the object reference + self.obj_ref = None + + def renew_subscription(self, lifetime): + if _debug: Subscription._debug("renew_subscription") + + # suspend iff scheduled + if self.isScheduled: + self.suspend_task() + + # reschedule the task if its not infinite + if lifetime != 0: + self.install_task(delta=lifetime) + + def process_task(self): + if _debug: Subscription._debug("process_task") + + # subscription is canceled + self.cancel_subscription() + +bacpypes_debugging(Subscription) + +# +# COVDetection +# + +class COVDetection(DetectionAlgorithm): + + properties_tracked = () + properties_reported = () + monitored_property_reference = None + + def __init__(self, obj): + if _debug: COVDetection._debug("__init__ %r", obj) + DetectionAlgorithm.__init__(self) + + # keep track of the object + self.obj = obj + + # build a list of parameters and matching object property references + kwargs = {} + for property_name in self.properties_tracked: + setattr(self, property_name, None) + kwargs[property_name] = (obj, property_name) + + # let the base class set up the bindings and initial values + self.bind(**kwargs) + + # list of all active subscriptions + self.cov_subscriptions = SubscriptionList() + + def execute(self): + if _debug: COVDetection._debug("execute") + + # something changed, send out the notifications + self.send_cov_notifications() + + def send_cov_notifications(self): + if _debug: COVDetection._debug("send_cov_notifications") + + # check for subscriptions + if not len(self.cov_subscriptions): + return + + # get the current time from the task manager + current_time = TaskManager().get_time() + if _debug: COVDetection._debug(" - current_time: %r", current_time) + + # create a list of values + list_of_values = [] + for property_name in self.properties_reported: + if _debug: COVDetection._debug(" - property_name: %r", property_name) + + # get the class + property_datatype = self.obj.get_datatype(property_name) + if _debug: COVDetection._debug(" - property_datatype: %r", property_datatype) + + # build the value + bundle_value = property_datatype(self.obj._values[property_name]) + if _debug: COVDetection._debug(" - bundle_value: %r", bundle_value) + + # bundle it into a sequence + property_value = PropertyValue( + propertyIdentifier=property_name, + value=Any(bundle_value), + ) + + # add it to the list + list_of_values.append(property_value) + if _debug: COVDetection._debug(" - list_of_values: %r", list_of_values) + + # loop through the subscriptions and send out notifications + for cov in self.cov_subscriptions: + if _debug: COVDetection._debug(" - cov: %s", repr(cov)) + + # calculate time remaining + if not cov.lifetime: + time_remaining = 0 + else: + time_remaining = int(cov.taskTime - current_time) + + # make sure it is at least one second + if not time_remaining: + time_remaining = 1 + + # build a request with the correct type + if cov.confirmed: + request = ConfirmedCOVNotificationRequest() + else: + request = UnconfirmedCOVNotificationRequest() + + # fill in the parameters + request.pduDestination = cov.client_addr + request.subscriberProcessIdentifier = cov.proc_id + request.initiatingDeviceIdentifier = self.obj._app.localDevice.objectIdentifier + request.monitoredObjectIdentifier = cov.obj_id + request.timeRemaining = time_remaining + request.listOfValues = list_of_values + if _debug: COVDetection._debug(" - request: %s", repr(request)) + + # let the application send it + self.obj._app.cov_notification(cov, request) + + def __str__(self): + return "<" + self.__class__.__name__ + \ + "(" + ','.join(self.properties_tracked) + ')' + \ + ">" + +bacpypes_debugging(COVDetection) + + +class GenericCriteria(COVDetection): + + properties_tracked = ( + 'presentValue', + 'statusFlags', + ) + properties_reported = ( + 'presentValue', + 'statusFlags', + ) + monitored_property_reference = 'presentValue' + + +class COVIncrementCriteria(COVDetection): + + properties_tracked = ( + 'presentValue', + 'statusFlags', + 'covIncrement', + ) + properties_reported = ( + 'presentValue', + 'statusFlags', + ) + monitored_property_reference = 'presentValue' + + def __init__(self, obj): + if _debug: COVIncrementCriteria._debug("__init__ %r", obj) + COVDetection.__init__(self, obj) + + # previous reported value + self.previous_reported_value = None + + @monitor_filter('presentValue') + def present_value_filter(self, old_value, new_value): + if _debug: COVIncrementCriteria._debug("present_value_filter %r %r", old_value, new_value) + + # first time around initialize to the old value + if self.previous_reported_value is None: + if _debug: COVIncrementCriteria._debug(" - first value: %r", old_value) + self.previous_reported_value = old_value + + # see if it changed enough to trigger reporting + value_changed = (new_value <= (self.previous_reported_value - self.covIncrement)) \ + or (new_value >= (self.previous_reported_value + self.covIncrement)) + if _debug: COVIncrementCriteria._debug(" - value significantly changed: %r", value_changed) + + return value_changed + + def send_cov_notifications(self): + if _debug: COVIncrementCriteria._debug("send_cov_notifications") + + # when sending out notifications, keep the current value + self.previous_reported_value = self.presentValue + + # continue + COVDetection.send_cov_notifications(self) + +bacpypes_debugging(COVDetection) + +class AccessDoorCriteria(COVDetection): + + properties_tracked = ( + 'presentValue', + 'statusFlags', + 'doorAlarmState', + ) + properties_reported = ( + 'presentValue', + 'statusFlags', + 'doorAlarmState', + ) + +class AccessPointCriteria(COVDetection): + + properties_tracked = ( + 'accessEventTime', + 'statusFlags', + ) + properties_reported = ( + 'accessEvent', + 'statusFlags', + 'accessEventTag', + 'accessEventTime', + 'accessEventCredential', + 'accessEventAuthenticationFactor', + ) + monitored_property_reference = 'accessEvent' + +class CredentialDataInputCriteria(COVDetection): + + properties_tracked = ( + 'updateTime', + 'statusFlags' + ) + properties_reported = ( + 'presentValue', + 'statusFlags', + 'updateTime', + ) + +class LoadControlCriteria(COVDetection): + + properties_tracked = ( + 'presentValue', + 'statusFlags', + 'requestedShedLevel', + 'startTime', + 'shedDuration', + 'dutyWindow', + ) + properties_reported = ( + 'presentValue', + 'statusFlags', + 'requestedShedLevel', + 'startTime', + 'shedDuration', + 'dutyWindow', + ) + +class PulseConverterCriteria(COVDetection): + + properties_tracked = ( + 'presentValue', + 'statusFlags', + ) + properties_reported = ( + 'presentValue', + 'statusFlags', + ) + +# mapping from object type to appropriate criteria class +criteria_type_map = { + 'accessPoint': AccessPointCriteria, + 'analogInput': COVIncrementCriteria, + 'analogOutput': COVIncrementCriteria, + 'analogValue': COVIncrementCriteria, + 'largeAnalogValue': COVIncrementCriteria, + 'integerValue': COVIncrementCriteria, + 'positiveIntegerValue': COVIncrementCriteria, + 'lightingOutput': COVIncrementCriteria, + 'binaryInput': GenericCriteria, + 'binaryOutput': GenericCriteria, + 'binaryValue': GenericCriteria, + 'lifeSafetyPoint': GenericCriteria, + 'lifeSafetyZone': GenericCriteria, + 'multiStateInput': GenericCriteria, + 'multiStateOutput': GenericCriteria, + 'multiStateValue': GenericCriteria, + 'octetString': GenericCriteria, + 'characterString': GenericCriteria, + 'timeValue': GenericCriteria, + 'dateTimeValue': GenericCriteria, + 'dateValue': GenericCriteria, + 'timePatternValue': GenericCriteria, + 'datePatternValue': GenericCriteria, + 'dateTimePatternValue': GenericCriteria, + 'credentialDataInput': CredentialDataInputCriteria, + 'loadControl': LoadControlCriteria, + 'pulseConverter': PulseConverterCriteria, + } + +# +# ActiveCOVSubscriptions +# + +class ActiveCOVSubscriptions(Property): + + def __init__(self): + Property.__init__( + self, 'activeCovSubscriptions', SequenceOf(COVSubscription), + default=None, optional=True, mutable=False, + ) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: ActiveCOVSubscriptions._debug("ReadProperty %s arrayIndex=%r", obj, arrayIndex) + + # get the current time from the task manager + current_time = TaskManager().get_time() + if _debug: ActiveCOVSubscriptions._debug(" - current_time: %r", current_time) + + # start with an empty sequence + cov_subscriptions = SequenceOf(COVSubscription)() + + # loop through the object and detection list + for obj, cov_detection in self.cov_detections.items(): + for cov in cov_detection.cov_subscriptions: + # calculate time remaining + if not cov.lifetime: + time_remaining = 0 + else: + time_remaining = int(cov.taskTime - current_time) + + # make sure it is at least one second + if not time_remaining: + time_remaining = 1 + + recipient_process = RecipientProcess( + recipient=Recipient( + address=DeviceAddress( + networkNumber=cov.client_addr.addrNet or 0, + macAddress=cov.client_addr.addrAddr, + ), + ), + processIdentifier=cov.proc_id, + ) + + cov_subscription = COVSubscription( + recipient=recipient_process, + monitoredPropertyReference=ObjectPropertyReference( + objectIdentifier=cov.obj_id, + propertyIdentifier=cov_detection.monitored_property_reference, + ), + issueConfirmedNotifications=cov.confirmed, + timeRemaining=time_remaining, + ) + if hasattr(cov_detection, 'covIncrement'): + cov_subscription.covIncrement = cov_detection.covIncrement + if _debug: ActiveCOVSubscriptions._debug(" - cov_subscription: %r", cov_subscription) + + # add the list + cov_subscriptions.append(cov_subscription) + + return cov_subscriptions + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +bacpypes_debugging(ActiveCOVSubscriptions) + +# +# ChangeOfValueServices +# + +class ChangeOfValueServices(Capability): + + def __init__(self): + if _debug: ChangeOfValueServices._debug("__init__") + Capability.__init__(self) + + # list of active subscriptions + self.active_cov_subscriptions = [] + + # map from an object to its detection algorithm + self.cov_detections = {} + + # if there is a local device object, make sure it has an active COV + # subscriptions property + if self.localDevice and self.localDevice.activeCovSubscriptions is None: + self.localDevice.add_property(ActiveCOVSubscriptions()) + + def add_subscription(self, cov): + if _debug: ChangeOfValueServices._debug("add_subscription %r", cov) + + # add it to the subscription list for its object + self.cov_detections[cov.obj_ref].cov_subscriptions.append(cov) + + def cancel_subscription(self, cov): + if _debug: ChangeOfValueServices._debug("cancel_subscription %r", cov) + + # cancel the subscription timeout + if cov.isScheduled: + cov.suspend_task() + if _debug: ChangeOfValueServices._debug(" - task suspended") + + # get the detection algorithm object + cov_detection = self.cov_detections[cov.obj_ref] + + # remove it from the subscription list for its object + cov_detection.cov_subscriptions.remove(cov) + + # if the detection algorithm doesn't have any subscriptions, remove it + if not len(cov_detection.cov_subscriptions): + if _debug: ChangeOfValueServices._debug(" - no more subscriptions") + + # unbind all the hooks into the object + cov_detection.unbind() + + # delete it from the object map + del self.cov_detections[cov.obj_ref] + + def cov_notification(self, cov, request): + if _debug: ChangeOfValueServices._debug("cov_notification %s %s", str(cov), str(request)) + + # create an IOCB with the request + iocb = IOCB(request) + if _debug: ChangeOfValueServices._debug(" - iocb: %r", iocb) + + # add a callback for the response, even if it was unconfirmed + iocb.cov = cov + iocb.add_callback(self.cov_confirmation) + + # send the request via the ApplicationIOController + self.request_io(iocb) + + def cov_confirmation(self, iocb): + if _debug: ChangeOfValueServices._debug("cov_confirmation %r", iocb) + + # do something for success + if iocb.ioResponse: + if _debug: ChangeOfValueServices._debug(" - ack") + self.cov_ack(iocb.cov, iocb.args[0], iocb.ioResponse) + + elif isinstance(iocb.ioError, Error): + if _debug: ChangeOfValueServices._debug(" - error: %r", iocb.ioError.errorCode) + self.cov_error(iocb.cov, iocb.args[0], iocb.ioError) + + elif isinstance(iocb.ioError, RejectPDU): + if _debug: ChangeOfValueServices._debug(" - reject: %r", iocb.ioError.apduAbortRejectReason) + self.cov_reject(iocb.cov, iocb.args[0], iocb.ioError) + + elif isinstance(iocb.ioError, AbortPDU): + if _debug: ChangeOfValueServices._debug(" - abort: %r", iocb.ioError.apduAbortRejectReason) + self.cov_abort(iocb.cov, iocb.args[0], iocb.ioError) + + def cov_ack(self, cov, request, response): + if _debug: ChangeOfValueServices._debug("cov_ack %r %r %r", cov, request, response) + + def cov_error(self, cov, request, response): + if _debug: ChangeOfValueServices._debug("cov_error %r %r %r", cov, request, response) + + def cov_reject(self, cov, request, response): + if _debug: ChangeOfValueServices._debug("cov_reject %r %r %r", cov, request, response) + + def cov_abort(self, cov, request, response): + if _debug: ChangeOfValueServices._debug("cov_abort %r %r %r", cov, request, response) + + ### delete the rest of the pending requests for this client + + def do_SubscribeCOVRequest(self, apdu): + if _debug: ChangeOfValueServices._debug("do_SubscribeCOVRequest %r", apdu) + + # extract the pieces + client_addr = apdu.pduSource + proc_id = apdu.subscriberProcessIdentifier + obj_id = apdu.monitoredObjectIdentifier + confirmed = apdu.issueConfirmedNotifications + lifetime = apdu.lifetime + + # request is to cancel the subscription + cancel_subscription = (confirmed is None) and (lifetime is None) + + # find the object + obj = self.get_object_id(obj_id) + if _debug: ChangeOfValueServices._debug(" - object: %r", obj) + if not obj: + raise ExecutionError(errorClass='object', errorCode='unknownObject') + + # look for an algorithm already associated with this object + cov_detection = self.cov_detections.get(obj, None) + + # if there isn't one, make one and associate it with the object + if not cov_detection: + # look for an associated class and if it's not there it's not supported + criteria_class = criteria_type_map.get(obj_id[0], None) + if not criteria_class: + raise ExecutionError(errorClass='services', errorCode='covSubscriptionFailed') + + # make one of these and bind it to the object + cov_detection = criteria_class(obj) + + # keep track of it for other subscriptions + self.cov_detections[obj] = cov_detection + if _debug: ChangeOfValueServices._debug(" - cov_detection: %r", cov_detection) + + # can a match be found? + cov = cov_detection.cov_subscriptions.find(client_addr, proc_id, obj_id) + if _debug: ChangeOfValueServices._debug(" - cov: %r", cov) + + # if a match was found, update the subscription + if cov: + if cancel_subscription: + if _debug: ChangeOfValueServices._debug(" - cancel the subscription") + self.cancel_subscription(cov) + else: + if _debug: ChangeOfValueServices._debug(" - renew the subscription") + cov.renew_subscription(lifetime) + else: + if cancel_subscription: + if _debug: ChangeOfValueServices._debug(" - cancel a subscription that doesn't exist") + else: + if _debug: ChangeOfValueServices._debug(" - create a subscription") + + # make a subscription + cov = Subscription(obj, client_addr, proc_id, obj_id, confirmed, lifetime) + if _debug: ChangeOfValueServices._debug(" - cov: %r", cov) + + # add it to our subscriptions lists + self.add_subscription(cov) + + # success + response = SimpleAckPDU(context=apdu) + + # return the result + self.response(response) + +bacpypes_debugging(ChangeOfValueServices) \ No newline at end of file diff --git a/py25/bacpypes/service/detect.py b/py25/bacpypes/service/detect.py new file mode 100755 index 00000000..4b743e85 --- /dev/null +++ b/py25/bacpypes/service/detect.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python + +""" +Detection +""" + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger + +from bacpypes.core import deferred + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# DetectionMonitor +# + +class DetectionMonitor: + + def __init__(self, algorithm, parameter, obj, prop, filter=None): + if _debug: DetectionMonitor._debug("__init__ ...") + + # keep track of the parameter values + self.algorithm = algorithm + self.parameter = parameter + self.obj = obj + self.prop = prop + self.filter = None + + def property_change(self, old_value, new_value): + if _debug: DetectionMonitor._debug("property_change %r %r", old_value, new_value) + + # set the parameter value + setattr(self.algorithm, self.parameter, new_value) + + # if the algorithm is already triggered, don't bother checking for more + if self.algorithm._triggered: + if _debug: DetectionMonitor._debug(" - already triggered") + return + + # if there is a special filter, use it, otherwise use != + if self.filter: + trigger = self.filter(old_value, new_value) + else: + trigger = (old_value != new_value) + if _debug: DetectionMonitor._debug(" - trigger: %r", trigger) + + # trigger it + if trigger: + deferred(self.algorithm._execute) + if _debug: DetectionMonitor._debug(" - deferred: %r", self.algorithm._execute) + + self.algorithm._triggered = True + +bacpypes_debugging(DetectionMonitor) + +# +# monitor_filter +# + +def monitor_filter(parameter): + def transfer_filter_decorator(fn): + fn._monitor_filter = parameter + return fn + + return transfer_filter_decorator + +# +# DetectionAlgorithm +# + +class DetectionAlgorithm: + + def __init__(self): + if _debug: DetectionAlgorithm._debug("__init__") + + # monitor objects + self._monitors = [] + + # triggered flag, set when a parameter changed and the monitor + # schedules the algorithm to execute + self._triggered = False + + def bind(self, **kwargs): + if _debug: DetectionAlgorithm._debug("bind %r", kwargs) + + # build a map of methods that are filters. These have been decorated + # with monitor_filter, but they are unbound methods (or simply + # functions in Python3) at the time they are decorated but by looking + # for them now they are bound to this instance. + monitor_filters = {} + for attr_name in dir(self): + attr = getattr(self, attr_name) + if hasattr(attr, "_monitor_filter"): + monitor_filters[attr._monitor_filter] = attr + if _debug: DetectionAlgorithm._debug(" - monitor_filters: %r", monitor_filters) + + for parameter, (obj, prop) in kwargs.items(): + if not hasattr(self, parameter): + if _debug: DetectionAlgorithm._debug(" - no matching parameter: %r", parameter) + + # make a detection monitor + monitor = DetectionMonitor(self, parameter, obj, prop) + if _debug: DetectionAlgorithm._debug(" - monitor: %r", monitor) + + # check to see if there is a custom filter for it + if parameter in monitor_filters: + monitor.filter = monitor_filters[parameter] + + # keep track of all of these objects for if/when we unbind + self._monitors.append(monitor) + + # add the property value monitor function + obj._property_monitors[prop].append(monitor.property_change) + + # set the parameter value to the property value if it's not None + property_value = obj._values[prop] + if property_value is not None: + if _debug: DetectionAlgorithm._debug(" - %s: %r", parameter, property_value) + setattr(self, parameter, property_value) + + def unbind(self): + if _debug: DetectionAlgorithm._debug("unbind") + + # remove the property value monitor functions + for monitor in self._monitors: + if _debug: DetectionAlgorithm._debug(" - monitor: %r", monitor) + monitor.obj._property_monitors[monitor.prop].remove(monitor.property_change) + + # abandon the array + self._monitors = [] + + def _execute(self): + if _debug: DetectionAlgorithm._debug("_execute") + + # provided by the derived class + self.execute() + + # turn the trigger off + self._triggered = False + + def execute(self): + raise NotImplementedError("execute not implemented") + +bacpypes_debugging(DetectionAlgorithm) diff --git a/py25/bacpypes/service/device.py b/py25/bacpypes/service/device.py new file mode 100644 index 00000000..659f7ebf --- /dev/null +++ b/py25/bacpypes/service/device.py @@ -0,0 +1,340 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger +from ..capability import Capability + +from ..pdu import GlobalBroadcast +from ..primitivedata import Date, Time, ObjectIdentifier +from ..constructeddata import ArrayOf + +from ..apdu import WhoIsRequest, IAmRequest, IHaveRequest +from ..errors import ExecutionError, InconsistentParameters, \ + MissingRequiredParameter, ParameterOutOfRange +from ..object import register_object_type, registered_object_types, \ + Property, DeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# CurrentDateProperty +# + +class CurrentDateProperty(Property): + + def __init__(self, identifier): + Property.__init__(self, identifier, Date, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + # access an array + if arrayIndex is not None: + raise TypeError("{0} is unsubscriptable".format(self.identifier)) + + # get the value + now = Date() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentTimeProperty +# + +class CurrentTimeProperty(Property): + + def __init__(self, identifier): + Property.__init__(self, identifier, Time, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + # access an array + if arrayIndex is not None: + raise TypeError("{0} is unsubscriptable".format(self.identifier)) + + # get the value + now = Time() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# LocalDeviceObject +# + +class LocalDeviceObject(DeviceObject): + + properties = \ + [ CurrentTimeProperty('localTime') + , CurrentDateProperty('localDate') + ] + + defaultProperties = \ + { 'maxApduLengthAccepted': 1024 + , 'segmentationSupported': 'segmentedBoth' + , 'maxSegmentsAccepted': 16 + , 'apduSegmentTimeout': 5000 + , 'apduTimeout': 3000 + , 'numberOfApduRetries': 3 + } + + def __init__(self, **kwargs): + if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) + + # fill in default property values not in kwargs + for attr, value in LocalDeviceObject.defaultProperties.items(): + if attr not in kwargs: + kwargs[attr] = value + + # check for registration + if self.__class__ not in registered_object_types.values(): + if 'vendorIdentifier' not in kwargs: + raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") + register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) + + # check for local time + if 'localDate' in kwargs: + raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") + if 'localTime' in kwargs: + raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") + + # check for a minimum value + if kwargs['maxApduLengthAccepted'] < 50: + raise ValueError("invalid max APDU length accepted") + + # dump the updated attributes + if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) + + # proceed as usual + DeviceObject.__init__(self, **kwargs) + + # create a default implementation of an object list for local devices. + # If it is specified in the kwargs, that overrides this default. + if ('objectList' not in kwargs): + self.objectList = ArrayOf(ObjectIdentifier)([self.objectIdentifier]) + + # if the object has a property list and one wasn't provided + # in the kwargs, then it was created by default and the objectList + # property should be included + if ('propertyList' not in kwargs) and self.propertyList: + # make sure it's not already there + if 'objectList' not in self.propertyList: + self.propertyList.append('objectList') + +# +# Who-Is I-Am Services +# + +class WhoIsIAmServices(Capability): + + def __init__(self): + if _debug: WhoIsIAmServices._debug("__init__") + Capability.__init__(self) + + def who_is(self, low_limit=None, high_limit=None, address=None): + if _debug: WhoIsIAmServices._debug("who_is") + + # build a request + whoIs = WhoIsRequest() + + # defaults to a global broadcast + if not address: + address = GlobalBroadcast() + + # set the destination + whoIs.pduDestination = address + + # check for consistent parameters + if (low_limit is not None): + if (high_limit is None): + raise MissingRequiredParameter("high_limit required") + if (low_limit < 0) or (low_limit > 4194303): + raise ParameterOutOfRange("low_limit out of range") + + # low limit is fine + whoIs.deviceInstanceRangeLowLimit = low_limit + + if (high_limit is not None): + if (low_limit is None): + raise MissingRequiredParameter("low_limit required") + if (high_limit < 0) or (high_limit > 4194303): + raise ParameterOutOfRange("high_limit out of range") + + # high limit is fine + whoIs.deviceInstanceRangeHighLimit = high_limit + + if _debug: WhoIsIAmServices._debug(" - whoIs: %r", whoIs) + + ### put the parameters someplace where they can be matched when the + ### appropriate I-Am comes in + + # away it goes + self.request(whoIs) + + def do_WhoIsRequest(self, apdu): + """Respond to a Who-Is request.""" + if _debug: WhoIsIAmServices._debug("do_WhoIsRequest %r", apdu) + + # ignore this if there's no local device + if not self.localDevice: + if _debug: WhoIsIAmServices._debug(" - no local device") + return + + # extract the parameters + low_limit = apdu.deviceInstanceRangeLowLimit + high_limit = apdu.deviceInstanceRangeHighLimit + + # check for consistent parameters + if (low_limit is not None): + if (high_limit is None): + raise MissingRequiredParameter("deviceInstanceRangeHighLimit required") + if (low_limit < 0) or (low_limit > 4194303): + raise ParameterOutOfRange("deviceInstanceRangeLowLimit out of range") + if (high_limit is not None): + if (low_limit is None): + raise MissingRequiredParameter("deviceInstanceRangeLowLimit required") + if (high_limit < 0) or (high_limit > 4194303): + raise ParameterOutOfRange("deviceInstanceRangeHighLimit out of range") + + # see we should respond + if (low_limit is not None): + if (self.localDevice.objectIdentifier[1] < low_limit): + return + if (high_limit is not None): + if (self.localDevice.objectIdentifier[1] > high_limit): + return + + # generate an I-Am + self.i_am(address=apdu.pduSource) + + def i_am(self, address=None): + if _debug: WhoIsIAmServices._debug("i_am") + + # this requires a local device + if not self.localDevice: + if _debug: WhoIsIAmServices._debug(" - no local device") + return + + # create a I-Am "response" back to the source + iAm = IAmRequest( + iAmDeviceIdentifier=self.localDevice.objectIdentifier, + maxAPDULengthAccepted=self.localDevice.maxApduLengthAccepted, + segmentationSupported=self.localDevice.segmentationSupported, + vendorID=self.localDevice.vendorIdentifier, + ) + + # defaults to a global broadcast + if not address: + address = GlobalBroadcast() + iAm.pduDestination = address + if _debug: WhoIsIAmServices._debug(" - iAm: %r", iAm) + + # away it goes + self.request(iAm) + + def do_IAmRequest(self, apdu): + """Respond to an I-Am request.""" + if _debug: WhoIsIAmServices._debug("do_IAmRequest %r", apdu) + + # check for required parameters + if apdu.iAmDeviceIdentifier is None: + raise MissingRequiredParameter("iAmDeviceIdentifier required") + if apdu.maxAPDULengthAccepted is None: + raise MissingRequiredParameter("maxAPDULengthAccepted required") + if apdu.segmentationSupported is None: + raise MissingRequiredParameter("segmentationSupported required") + if apdu.vendorID is None: + raise MissingRequiredParameter("vendorID required") + + # extract the device instance number + device_instance = apdu.iAmDeviceIdentifier[1] + if _debug: WhoIsIAmServices._debug(" - device_instance: %r", device_instance) + + # extract the source address + device_address = apdu.pduSource + if _debug: WhoIsIAmServices._debug(" - device_address: %r", device_address) + + ### check to see if the application is looking for this device + ### and update the device info cache if it is + +bacpypes_debugging(WhoIsIAmServices) + +# +# Who-Has I-Have Services +# + +class WhoHasIHaveServices(Capability): + + def __init__(self): + if _debug: WhoHasIHaveServices._debug("__init__") + Capability.__init__(self) + + def who_has(self, thing, address=None): + if _debug: WhoHasIHaveServices._debug("who_has %r address=%r", thing, address) + + raise NotImplementedError("who_has") + + def do_WhoHasRequest(self, apdu): + """Respond to a Who-Has request.""" + if _debug: WhoHasIHaveServices._debug("do_WhoHasRequest, %r", apdu) + + # ignore this if there's no local device + if not self.localDevice: + if _debug: WhoIsIAmServices._debug(" - no local device") + return + + # find the object + if apdu.object.objectIdentifier is not None: + obj = self.objectIdentifier.get(apdu.object.objectIdentifier, None) + elif apdu.object.objectName is not None: + obj = self.objectName.get(apdu.object.objectName, None) + else: + raise InconsistentParameters("object identifier or object name required") + if not obj: + raise ExecutionError(errorClass='object', errorCode='unknownObject') + + # send out the response + self.i_have(obj, address=apdu.pduSource) + + def i_have(self, thing, address=None): + if _debug: WhoHasIHaveServices._debug("i_have %r address=%r", thing, address) + + # ignore this if there's no local device + if not self.localDevice: + if _debug: WhoIsIAmServices._debug(" - no local device") + return + + # build the request + iHave = IHaveRequest( + deviceIdentifier=self.localDevice.objectIdentifier, + objectIdentifier=thing.objectIdentifier, + objectName=thing.objectName, + ) + + # defaults to a global broadcast + if not address: + address = GlobalBroadcast() + iHave.pduDestination = address + if _debug: WhoHasIHaveServices._debug(" - iHave: %r", iHave) + + # send it along + self.request(iHave) + + def do_IHaveRequest(self, apdu): + """Respond to a I-Have request.""" + if _debug: WhoHasIHaveServices._debug("do_IHaveRequest %r", apdu) + + # check for required parameters + if apdu.deviceIdentifier is None: + raise MissingRequiredParameter("deviceIdentifier required") + if apdu.objectIdentifier is None: + raise MissingRequiredParameter("objectIdentifier required") + if apdu.objectName is None: + raise MissingRequiredParameter("objectName required") + + ### check to see if the application is looking for this object + +bacpypes_debugging(WhoHasIHaveServices) diff --git a/py25/bacpypes/service/file.py b/py25/bacpypes/service/file.py new file mode 100644 index 00000000..3f7be2f2 --- /dev/null +++ b/py25/bacpypes/service/file.py @@ -0,0 +1,300 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger +from ..capability import Capability + +from ..object import FileObject + +from ..apdu import AtomicReadFileACK, AtomicReadFileACKAccessMethodChoice, \ + AtomicReadFileACKAccessMethodRecordAccess, \ + AtomicReadFileACKAccessMethodStreamAccess, \ + AtomicWriteFileACK +from ..errors import ExecutionError, MissingRequiredParameter + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# Local Record Access File Object Type +# + +class LocalRecordAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a record accessed file object. """ + if _debug: + LocalRecordAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'recordAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'recordAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of records. """ + raise NotImplementedError("__len__") + + def read_record(self, start_record, record_count): + """ Read a number of records starting at a specific record. """ + raise NotImplementedError("read_record") + + def write_record(self, start_record, record_count, record_data): + """ Write a number of records, starting at a specific record. """ + raise NotImplementedError("write_record") + +bacpypes_debugging(LocalRecordAccessFileObject) + +# +# Local Stream Access File Object Type +# + +class LocalStreamAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a stream accessed file object. """ + if _debug: + LocalStreamAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'streamAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'streamAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of octets in the file. """ + raise NotImplementedError("write_file") + + def read_stream(self, start_position, octet_count): + """ Read a chunk of data out of the file. """ + raise NotImplementedError("read_stream") + + def write_stream(self, start_position, data): + """ Write a number of octets, starting at a specific offset. """ + raise NotImplementedError("write_stream") + +bacpypes_debugging(LocalStreamAccessFileObject) + +# +# File Application Mixin +# + +class FileServices(Capability): + + def __init__(self): + if _debug: FileServices._debug("__init__") + Capability.__init__(self) + + def do_AtomicReadFileRequest(self, apdu): + """Return one of our records.""" + if _debug: FileServices._debug("do_AtomicReadFileRequest %r", apdu) + + if (apdu.fileIdentifier[0] != 'file'): + raise ExecutionError('services', 'inconsistentObjectType') + + # get the object + obj = self.get_object_id(apdu.fileIdentifier) + if _debug: FileServices._debug(" - object: %r", obj) + + if not obj: + raise ExecutionError('object', 'unknownObject') + + if apdu.accessMethod.recordAccess: + # check against the object + if obj.fileAccessMethod != 'recordAccess': + raise ExecutionError('services', 'invalidFileAccessMethod') + + # simplify + record_access = apdu.accessMethod.recordAccess + + # check for required parameters + if record_access.fileStartRecord is None: + raise MissingRequiredParameter("fileStartRecord required") + if record_access.requestedRecordCount is None: + raise MissingRequiredParameter("requestedRecordCount required") + + ### verify start is valid - double check this (empty files?) + if (record_access.fileStartRecord < 0) or \ + (record_access.fileStartRecord >= len(obj)): + raise ExecutionError('services', 'invalidFileStartPosition') + + # pass along to the object + end_of_file, record_data = obj.read_record( + record_access.fileStartRecord, + record_access.requestedRecordCount, + ) + if _debug: FileServices._debug(" - record_data: %r", record_data) + + # this is an ack + resp = AtomicReadFileACK(context=apdu, + endOfFile=end_of_file, + accessMethod=AtomicReadFileACKAccessMethodChoice( + recordAccess=AtomicReadFileACKAccessMethodRecordAccess( + fileStartRecord=record_access.fileStartRecord, + returnedRecordCount=len(record_data), + fileRecordData=record_data, + ), + ), + ) + + elif apdu.accessMethod.streamAccess: + # check against the object + if obj.fileAccessMethod != 'streamAccess': + raise ExecutionError('services', 'invalidFileAccessMethod') + + # simplify + stream_access = apdu.accessMethod.streamAccess + + # check for required parameters + if stream_access.fileStartPosition is None: + raise MissingRequiredParameter("fileStartPosition required") + if stream_access.requestedOctetCount is None: + raise MissingRequiredParameter("requestedOctetCount required") + + ### verify start is valid - double check this (empty files?) + if (stream_access.fileStartPosition < 0) or \ + (stream_access.fileStartPosition >= len(obj)): + raise ExecutionError('services', 'invalidFileStartPosition') + + # pass along to the object + end_of_file, record_data = obj.read_stream( + stream_access.fileStartPosition, + stream_access.requestedOctetCount, + ) + if _debug: FileServices._debug(" - record_data: %r", record_data) + + # this is an ack + resp = AtomicReadFileACK(context=apdu, + endOfFile=end_of_file, + accessMethod=AtomicReadFileACKAccessMethodChoice( + streamAccess=AtomicReadFileACKAccessMethodStreamAccess( + fileStartPosition=stream_access.fileStartPosition, + fileData=record_data, + ), + ), + ) + + if _debug: FileServices._debug(" - resp: %r", resp) + + # return the result + self.response(resp) + + def do_AtomicWriteFileRequest(self, apdu): + """Return one of our records.""" + if _debug: FileServices._debug("do_AtomicWriteFileRequest %r", apdu) + + if (apdu.fileIdentifier[0] != 'file'): + raise ExecutionError('services', 'inconsistentObjectType') + + # get the object + obj = self.get_object_id(apdu.fileIdentifier) + if _debug: FileServices._debug(" - object: %r", obj) + + if not obj: + raise ExecutionError('object', 'unknownObject') + + if apdu.accessMethod.recordAccess: + # check against the object + if obj.fileAccessMethod != 'recordAccess': + raise ExecutionError('services', 'invalidFileAccessMethod') + + # simplify + record_access = apdu.accessMethod.recordAccess + + # check for required parameters + if record_access.fileStartRecord is None: + raise MissingRequiredParameter("fileStartRecord required") + if record_access.recordCount is None: + raise MissingRequiredParameter("recordCount required") + if record_access.fileRecordData is None: + raise MissingRequiredParameter("fileRecordData required") + + # check for read-only + if obj.readOnly: + raise ExecutionError('services', 'fileAccessDenied') + + # pass along to the object + start_record = obj.write_record( + record_access.fileStartRecord, + record_access.recordCount, + record_access.fileRecordData, + ) + if _debug: FileServices._debug(" - start_record: %r", start_record) + + # this is an ack + resp = AtomicWriteFileACK(context=apdu, + fileStartRecord=start_record, + ) + + elif apdu.accessMethod.streamAccess: + # check against the object + if obj.fileAccessMethod != 'streamAccess': + raise ExecutionError('services', 'invalidFileAccessMethod') + + # simplify + stream_access = apdu.accessMethod.streamAccess + + # check for required parameters + if stream_access.fileStartPosition is None: + raise MissingRequiredParameter("fileStartPosition required") + if stream_access.fileData is None: + raise MissingRequiredParameter("fileData required") + + # check for read-only + if obj.readOnly: + raise ExecutionError('services', 'fileAccessDenied') + + # pass along to the object + start_position = obj.write_stream( + stream_access.fileStartPosition, + stream_access.fileData, + ) + if _debug: FileServices._debug(" - start_position: %r", start_position) + + # this is an ack + resp = AtomicWriteFileACK(context=apdu, + fileStartPosition=start_position, + ) + + if _debug: FileServices._debug(" - resp: %r", resp) + + # return the result + self.response(resp) + +bacpypes_debugging(FileServices) + +# +# FileServicesClient +# + +class FileServicesClient(Capability): + + def read_record(self, address, fileIdentifier, start_record, record_count): + """ Read a number of records starting at a specific record. """ + raise NotImplementedError("read_record") + + def write_record(self, address, fileIdentifier, start_record, record_count, record_data): + """ Write a number of records, starting at a specific record. """ + raise NotImplementedError("write_record") + + def read_stream(self, address, fileIdentifier, start_position, octet_count): + """ Read a chunk of data out of the file. """ + raise NotImplementedError("read_stream") + + def write_stream(self, address, fileIdentifier, start_position, data): + """ Write a number of octets, starting at a specific offset. """ + raise NotImplementedError("write_stream") diff --git a/py25/bacpypes/service/object.py b/py25/bacpypes/service/object.py new file mode 100755 index 00000000..f6cdc403 --- /dev/null +++ b/py25/bacpypes/service/object.py @@ -0,0 +1,331 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger +from ..capability import Capability + +from ..basetypes import ErrorType +from ..primitivedata import Atomic, Null, Unsigned +from ..constructeddata import Any, Array + +from ..apdu import Error, \ + SimpleAckPDU, ReadPropertyACK, ReadPropertyMultipleACK, \ + ReadAccessResult, ReadAccessResultElement, ReadAccessResultElementChoice +from ..errors import ExecutionError +from ..object import PropertyError + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# ReadProperty and WriteProperty Services +# + +class ReadWritePropertyServices(Capability): + + def __init__(self): + if _debug: ReadWritePropertyServices._debug("__init__") + Capability.__init__(self) + + def do_ReadPropertyRequest(self, apdu): + """Return the value of some property of one of our objects.""" + if _debug: ReadWritePropertyServices._debug("do_ReadPropertyRequest %r", apdu) + + # extract the object identifier + objId = apdu.objectIdentifier + + # check for wildcard + if (objId == ('device', 4194303)) and self.localDevice is not None: + if _debug: ReadWritePropertyServices._debug(" - wildcard device identifier") + objId = self.localDevice.objectIdentifier + + # get the object + obj = self.get_object_id(objId) + if _debug: ReadWritePropertyServices._debug(" - object: %r", obj) + + if not obj: + raise ExecutionError(errorClass='object', errorCode='unknownObject') + + try: + # get the datatype + datatype = obj.get_datatype(apdu.propertyIdentifier) + if _debug: ReadWritePropertyServices._debug(" - datatype: %r", datatype) + + # get the value + value = obj.ReadProperty(apdu.propertyIdentifier, apdu.propertyArrayIndex) + if _debug: ReadWritePropertyServices._debug(" - value: %r", value) + if value is None: + raise PropertyError(apdu.propertyIdentifier) + + # change atomic values into something encodeable + if issubclass(datatype, Atomic): + value = datatype(value) + elif issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): + if apdu.propertyArrayIndex == 0: + value = Unsigned(value) + elif issubclass(datatype.subtype, Atomic): + value = datatype.subtype(value) + elif not isinstance(value, datatype.subtype): + raise TypeError("invalid result datatype, expecting {0} and got {1}" \ + .format(datatype.subtype.__name__, type(value).__name__)) + elif not isinstance(value, datatype): + raise TypeError("invalid result datatype, expecting {0} and got {1}" \ + .format(datatype.__name__, type(value).__name__)) + if _debug: ReadWritePropertyServices._debug(" - encodeable value: %r", value) + + # this is a ReadProperty ack + resp = ReadPropertyACK(context=apdu) + resp.objectIdentifier = objId + resp.propertyIdentifier = apdu.propertyIdentifier + resp.propertyArrayIndex = apdu.propertyArrayIndex + + # save the result in the property value + resp.propertyValue = Any() + resp.propertyValue.cast_in(value) + if _debug: ReadWritePropertyServices._debug(" - resp: %r", resp) + + except PropertyError: + raise ExecutionError(errorClass='object', errorCode='unknownProperty') + + # return the result + self.response(resp) + + def do_WritePropertyRequest(self, apdu): + """Change the value of some property of one of our objects.""" + if _debug: ReadWritePropertyServices._debug("do_WritePropertyRequest %r", apdu) + + # get the object + obj = self.get_object_id(apdu.objectIdentifier) + if _debug: ReadWritePropertyServices._debug(" - object: %r", obj) + if not obj: + raise ExecutionError(errorClass='object', errorCode='unknownObject') + + try: + # check if the property exists + if obj.ReadProperty(apdu.propertyIdentifier, apdu.propertyArrayIndex) is None: + raise PropertyError(apdu.propertyIdentifier) + + # get the datatype, special case for null + if apdu.propertyValue.is_application_class_null(): + datatype = Null + else: + datatype = obj.get_datatype(apdu.propertyIdentifier) + if _debug: ReadWritePropertyServices._debug(" - datatype: %r", datatype) + + # special case for array parts, others are managed by cast_out + if issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): + if apdu.propertyArrayIndex == 0: + value = apdu.propertyValue.cast_out(Unsigned) + else: + value = apdu.propertyValue.cast_out(datatype.subtype) + else: + value = apdu.propertyValue.cast_out(datatype) + if _debug: ReadWritePropertyServices._debug(" - value: %r", value) + + # change the value + value = obj.WriteProperty(apdu.propertyIdentifier, value, apdu.propertyArrayIndex, apdu.priority) + + # success + resp = SimpleAckPDU(context=apdu) + if _debug: ReadWritePropertyServices._debug(" - resp: %r", resp) + + except PropertyError: + raise ExecutionError(errorClass='object', errorCode='unknownProperty') + + # return the result + self.response(resp) + +bacpypes_debugging(ReadWritePropertyServices) + +# +# read_property_to_any +# + +def read_property_to_any(obj, propertyIdentifier, propertyArrayIndex=None): + """Read the specified property of the object, with the optional array index, + and cast the result into an Any object.""" + if _debug: read_property_to_any._debug("read_property_to_any %s %r %r", obj, propertyIdentifier, propertyArrayIndex) + + # get the datatype + datatype = obj.get_datatype(propertyIdentifier) + if _debug: read_property_to_any._debug(" - datatype: %r", datatype) + if datatype is None: + raise ExecutionError(errorClass='property', errorCode='datatypeNotSupported') + + # get the value + value = obj.ReadProperty(propertyIdentifier, propertyArrayIndex) + if _debug: read_property_to_any._debug(" - value: %r", value) + if value is None: + raise ExecutionError(errorClass='property', errorCode='unknownProperty') + + # change atomic values into something encodeable + if issubclass(datatype, Atomic): + value = datatype(value) + elif issubclass(datatype, Array) and (propertyArrayIndex is not None): + if propertyArrayIndex == 0: + value = Unsigned(value) + elif issubclass(datatype.subtype, Atomic): + value = datatype.subtype(value) + elif not isinstance(value, datatype.subtype): + raise TypeError("invalid result datatype, expecting %s and got %s" \ + % (datatype.subtype.__name__, type(value).__name__)) + elif not isinstance(value, datatype): + raise TypeError("invalid result datatype, expecting %s and got %s" \ + % (datatype.__name__, type(value).__name__)) + if _debug: read_property_to_any._debug(" - encodeable value: %r", value) + + # encode the value + result = Any() + result.cast_in(value) + if _debug: read_property_to_any._debug(" - result: %r", result) + + # return the object + return result + +bacpypes_debugging(read_property_to_any) + +# +# read_property_to_result_element +# + +def read_property_to_result_element(obj, propertyIdentifier, propertyArrayIndex=None): + """Read the specified property of the object, with the optional array index, + and cast the result into an Any object.""" + if _debug: read_property_to_result_element._debug("read_property_to_result_element %s %r %r", obj, propertyIdentifier, propertyArrayIndex) + + # save the result in the property value + read_result = ReadAccessResultElementChoice() + + try: + read_result.propertyValue = read_property_to_any(obj, propertyIdentifier, propertyArrayIndex) + if _debug: read_property_to_result_element._debug(" - success") + except PropertyError, error: + if _debug: read_property_to_result_element._debug(" - error: %r", error) + read_result.propertyAccessError = ErrorType(errorClass='property', errorCode='unknownProperty') + except ExecutionError, error: + if _debug: read_property_to_result_element._debug(" - error: %r", error) + read_result.propertyAccessError = ErrorType(errorClass=error.errorClass, errorCode=error.errorCode) + + # make an element for this value + read_access_result_element = ReadAccessResultElement( + propertyIdentifier=propertyIdentifier, + propertyArrayIndex=propertyArrayIndex, + readResult=read_result, + ) + if _debug: read_property_to_result_element._debug(" - read_access_result_element: %r", read_access_result_element) + + # fini + return read_access_result_element + +bacpypes_debugging(read_property_to_result_element) + +# +# ReadWritePropertyMultipleServices +# + +class ReadWritePropertyMultipleServices(Capability): + + def __init__(self): + if _debug: ReadWritePropertyMultipleServices._debug("__init__") + Capability.__init__(self) + + def do_ReadPropertyMultipleRequest(self, apdu): + """Respond to a ReadPropertyMultiple Request.""" + if _debug: ReadWritePropertyMultipleServices._debug("do_ReadPropertyMultipleRequest %r", apdu) + + # response is a list of read access results (or an error) + resp = None + read_access_result_list = [] + + # loop through the request + for read_access_spec in apdu.listOfReadAccessSpecs: + # get the object identifier + objectIdentifier = read_access_spec.objectIdentifier + if _debug: ReadWritePropertyMultipleServices._debug(" - objectIdentifier: %r", objectIdentifier) + + # check for wildcard + if (objectIdentifier == ('device', 4194303)) and self.localDevice is not None: + if _debug: ReadWritePropertyMultipleServices._debug(" - wildcard device identifier") + objectIdentifier = self.localDevice.objectIdentifier + + # get the object + obj = self.get_object_id(objectIdentifier) + if _debug: ReadWritePropertyMultipleServices._debug(" - object: %r", obj) + + # make sure it exists + if not obj: + resp = Error(errorClass='object', errorCode='unknownObject', context=apdu) + if _debug: ReadWritePropertyMultipleServices._debug(" - unknown object error: %r", resp) + break + + # build a list of result elements + read_access_result_element_list = [] + + # loop through the property references + for prop_reference in read_access_spec.listOfPropertyReferences: + # get the property identifier + propertyIdentifier = prop_reference.propertyIdentifier + if _debug: ReadWritePropertyMultipleServices._debug(" - propertyIdentifier: %r", propertyIdentifier) + + # get the array index (optional) + propertyArrayIndex = prop_reference.propertyArrayIndex + if _debug: ReadWritePropertyMultipleServices._debug(" - propertyArrayIndex: %r", propertyArrayIndex) + + # check for special property identifiers + if propertyIdentifier in ('all', 'required', 'optional'): + for propId, prop in obj._properties.items(): + if _debug: ReadWritePropertyMultipleServices._debug(" - checking: %r %r", propId, prop.optional) + + if (propertyIdentifier == 'all'): + pass + elif (propertyIdentifier == 'required') and (prop.optional): + if _debug: ReadWritePropertyMultipleServices._debug(" - not a required property") + continue + elif (propertyIdentifier == 'optional') and (not prop.optional): + if _debug: ReadWritePropertyMultipleServices._debug(" - not an optional property") + continue + + # read the specific property + read_access_result_element = read_property_to_result_element(obj, propId, propertyArrayIndex) + + # check for undefined property + if read_access_result_element.readResult.propertyAccessError \ + and read_access_result_element.readResult.propertyAccessError.errorCode == 'unknownProperty': + continue + + # add it to the list + read_access_result_element_list.append(read_access_result_element) + + else: + # read the specific property + read_access_result_element = read_property_to_result_element(obj, propertyIdentifier, propertyArrayIndex) + + # add it to the list + read_access_result_element_list.append(read_access_result_element) + + # build a read access result + read_access_result = ReadAccessResult( + objectIdentifier=objectIdentifier, + listOfResults=read_access_result_element_list + ) + if _debug: ReadWritePropertyMultipleServices._debug(" - read_access_result: %r", read_access_result) + + # add it to the list + read_access_result_list.append(read_access_result) + + # this is a ReadPropertyMultiple ack + if not resp: + resp = ReadPropertyMultipleACK(context=apdu) + resp.listOfReadAccessResults = read_access_result_list + if _debug: ReadWritePropertyMultipleServices._debug(" - resp: %r", resp) + + # return the result + self.response(resp) + +# def do_WritePropertyMultipleRequest(self, apdu): +# """Respond to a WritePropertyMultiple Request.""" +# if _debug: ReadWritePropertyMultipleServices._debug("do_ReadPropertyMultipleRequest %r", apdu) +# +# raise NotImplementedError() + +bacpypes_debugging(ReadWritePropertyMultipleServices) diff --git a/py25/bacpypes/service/test.py b/py25/bacpypes/service/test.py new file mode 100644 index 00000000..79058f98 --- /dev/null +++ b/py25/bacpypes/service/test.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +""" +Test Service +""" + +from ..debugging import bacpypes_debugging, ModuleLogger + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +def some_function(*args): + if _debug: some_function._debug("f %r", args) + + return args[0] + 1 + +bacpypes_debugging(some_function) \ No newline at end of file diff --git a/py25/bacpypes/tcp.py b/py25/bacpypes/tcp.py index 0e3639be..964dce6a 100755 --- a/py25/bacpypes/tcp.py +++ b/py25/bacpypes/tcp.py @@ -104,66 +104,106 @@ def __init__(self, peer): # create a request buffer self.request = '' - # hold the socket error if there was one - self.socketError = None + # try to connect + try: + if _debug: TCPClient._debug(" - initiate connection") + self.connect(peer) + except socket.error, err: + if _debug: TCPClient._debug(" - connect socket error: %r", err) - # try to connect the socket - if _debug: TCPClient._debug(" - try to connect") - self.connect(peer) - if _debug: TCPClient._debug(" - connected (maybe)") + # pass along to an error handler + self.handle_error(err) def handle_connect(self): - if _debug: deferred(TCPClient._debug, "handle_connect") + if _debug: TCPClient._debug("handle_connect") + + def handle_connect_event(self): + if _debug: TCPClient._debug("handle_connect_event") + + # there might be an error + err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + if _debug: TCPClient._debug(" - err: %r", err) + + # check for connection refused + if (err == 0): + if _debug: TCPClient._debug(" - no error") + elif (err == 111): + if _debug: TCPClient._debug(" - connection to %r refused", self.peer) + self.handle_error(socket.error(111, "connection refused")) + return - def handle_expt(self): - pass + # pass along + asyncore.dispatcher.handle_connect_event(self) def readable(self): - return 1 + return self.connected def handle_read(self): - if _debug: deferred(TCPClient._debug, "handle_read") + if _debug: TCPClient._debug("handle_read") try: msg = self.recv(65536) - if _debug: deferred(TCPClient._debug, " - received %d octets", len(msg)) - self.socketError = None + if _debug: TCPClient._debug(" - received %d octets", len(msg)) # no socket means it was closed if not self.socket: - if _debug: deferred(TCPClient._debug, " - socket was closed") + if _debug: TCPClient._debug(" - socket was closed") else: - # sent the data upstream + # send the data upstream deferred(self.response, PDU(msg)) except socket.error, err: - if (err.args[0] == 111): - deferred(TCPClient._error, "connection to %r refused", self.peer) + if (err.args[0] in (61, 111)): + if _debug: TCPClient._debug(" - connection to %r refused", self.peer) else: - deferred(TCPClient._error, "TCPClient.handle_read socket error: %r", err) - self.socketError = err + if _debug: TCPClient._debug(" - recv socket error: %r", err) + + # pass along to a handler + self.handle_error(err) def writable(self): return (len(self.request) != 0) def handle_write(self): - if _debug: deferred(TCPClient._debug, "handle_write") + if _debug: TCPClient._debug("handle_write") try: sent = self.send(self.request) - if _debug: deferred(TCPClient._debug, " - sent %d octets, %d remaining", sent, len(self.request) - sent) - self.socketError = None + if _debug: TCPClient._debug(" - sent %d octets, %d remaining", sent, len(self.request) - sent) self.request = self.request[sent:] + except socket.error, err: - if (err.args[0] == 111): - deferred(TCPClient._error, "connection to %r refused", self.peer) + if (err.args[0] == 32): + if _debug: TCPClient._debug(" - broken pipe to %r", self.peer) + return + elif (err.args[0] in (61, 111)): + if _debug: TCPClient._debug(" - connection to %r refused", self.peer) else: - deferred(TCPClient._error, "handle_write socket error: %s", err) - self.socketError = err + if _debug: TCPClient._debug(" - send socket error: %s", err) + + # pass along to a handler + self.handle_error(err) + + def handle_write_event(self): + if _debug: TCPClient._debug("handle_write_event") + + # there might be an error + err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + if _debug: TCPClient._debug(" - err: %r", err) + + # check for connection refused + if (err in (61, 111)): + if _debug: TCPClient._debug(" - connection to %r refused", self.peer) + self.handle_error(socket.error(err, "connection refused")) + self.handle_close() + return + + # pass along + asyncore.dispatcher.handle_write_event(self) def handle_close(self): - if _debug: deferred(TCPClient._debug, "handle_close") + if _debug: TCPClient._debug("handle_close") # close the socket self.close() @@ -171,6 +211,13 @@ def handle_close(self): # make sure other routines know the socket is closed self.socket = None + def handle_error(self, error=None): + """Trap for TCPClient errors, otherwise continue.""" + if _debug: TCPClient._debug("handle_error %r", error) + + # core does not take parameters + asyncore.dispatcher.handle_error(self) + def indication(self, pdu): """Requests are queued for delivery.""" if _debug: TCPClient._debug("indication %r", pdu) @@ -209,6 +256,16 @@ def __init__(self, director, peer): # tell the director this is a new actor self.director.add_actor(self) + def handle_error(self, error=None): + """Trap for TCPClient errors, otherwise continue.""" + if _debug: TCPClientActor._debug("handle_error %r", error) + + # pass along to the director + if error is not None: + self.director.actor_error(self, error) + else: + TCPClient.handle_error(self) + def handle_close(self): if _debug: TCPClientActor._debug("handle_close") @@ -221,7 +278,7 @@ def handle_close(self): self.timer.suspend_task() # tell the director this is gone - self.director.remove_actor(self) + self.director.del_actor(self) # pass the function along TCPClient.handle_close(self) @@ -324,23 +381,30 @@ def add_actor(self, actor): # tell the ASE there is a new client if self.serviceElement: - self.sap_request(addPeer=actor.peer) + self.sap_request(add_actor=actor) - def remove_actor(self, actor): + def del_actor(self, actor): """Remove an actor when the socket is closed.""" - if _debug: TCPClientDirector._debug("remove_actor %r", actor) + if _debug: TCPClientDirector._debug("del_actor %r", actor) del self.clients[actor.peer] # tell the ASE the client has gone away if self.serviceElement: - self.sap_request(delPeer=actor.peer) + self.sap_request(del_actor=actor) # see if it should be reconnected if actor.peer in self.reconnect: connect_task = FunctionTask(self.connect, actor.peer) connect_task.install_task(_time() + self.reconnect[actor.peer]) + def actor_error(self, actor, error): + if _debug: TCPClientDirector._debug("actor_error %r %r", actor, error) + + # tell the ASE the actor had an error + if self.serviceElement: + self.sap_request(actor_error=actor, error=error) + def get_actor(self, address): """ Get the actor associated with an address or None. """ return self.clients.get(address, None) @@ -404,68 +468,76 @@ def __init__(self, sock, peer): # create a request buffer self.request = '' - # hold the socket error if there was one - self.socketError = None - def handle_connect(self): - if _debug: deferred(TCPServer._debug, "handle_connect") + if _debug: TCPServer._debug("handle_connect") def readable(self): return 1 def handle_read(self): - if _debug: deferred(TCPServer._debug, "handle_read") + if _debug: TCPServer._debug("handle_read") try: msg = self.recv(65536) - if _debug: deferred(TCPServer._debug, " - received %d octets", len(msg)) - self.socketError = None + if _debug: TCPServer._debug(" - received %d octets", len(msg)) # no socket means it was closed if not self.socket: - if _debug: deferred(TCPServer._debug, " - socket was closed") + if _debug: TCPServer._debug(" - socket was closed") else: + # send the data upstream deferred(self.response, PDU(msg)) except socket.error, err: if (err.args[0] == 111): - deferred(TCPServer._error, "connection to %r refused", self.peer) + if _debug: TCPServer._debug(" - connection to %r refused", self.peer) else: - deferred(TCPServer._error, "handle_read socket error: %s", err) - self.socketError = err + if _debug: TCPServer._debug(" - recv socket error: %s", err) + + # pass along to a handler + self.handle_error(err) def writable(self): return (len(self.request) != 0) def handle_write(self): - if _debug: deferred(TCPServer._debug, "handle_write") + if _debug: TCPServer._debug("handle_write") try: sent = self.send(self.request) - if _debug: deferred(TCPServer._debug, " - sent %d octets, %d remaining", sent, len(self.request) - sent) - self.socketError = None + if _debug: TCPServer._debug(" - sent %d octets, %d remaining", sent, len(self.request) - sent) self.request = self.request[sent:] - except socket.error, why: - if (why.args[0] == 111): - deferred(TCPServer._error, "connection to %r refused", self.peer) + + except socket.error, err: + if (err.args[0] == 111): + if _debug: TCPServer._debug(" - connection to %r refused", self.peer) else: - deferred(TCPServer._error, "handle_write socket error: %s", why) - self.socketError = why + if _debug: TCPServer._debug(" - send socket error: %s", err) + + # pass along to a handler + self.handle_error(err) def handle_close(self): - if _debug: deferred(TCPServer._debug, "handle_close") + if _debug: TCPServer._debug("handle_close") if not self: - deferred(TCPServer._warning, "handle_close: self is None") + if _debug: TCPServer._debug(" - self is None") return if not self.socket: - deferred(TCPServer._warning, "handle_close: socket already closed") + if _debug: TCPServer._debug(" - socket already closed") return self.close() self.socket = None + def handle_error(self, error=None): + """Trap for TCPServer errors, otherwise continue.""" + if _debug: TCPServer._debug("handle_error %r", error) + + # core does not take parameters + asyncore.dispatcher.handle_error(self) + def indication(self, pdu): """Requests are queued for delivery.""" if _debug: TCPServer._debug("indication %r", pdu) @@ -501,6 +573,16 @@ def __init__(self, director, sock, peer): # tell the director this is a new actor self.director.add_actor(self) + def handle_error(self, error=None): + """Trap for TCPServer errors, otherwise continue.""" + if _debug: TCPServerActor._debug("handle_error %r", error) + + # pass along to the director + if error is not None: + self.director.actor_error(self, error) + else: + TCPServer.handle_error(self) + def handle_close(self): if _debug: TCPServerActor._debug("handle_close") @@ -509,7 +591,7 @@ def handle_close(self): self.flushTask.suspend_task() # tell the director this is gone - self.director.remove_actor(self) + self.director.del_actor(self) # pass it down TCPServer.handle_close(self) @@ -667,19 +749,26 @@ def add_actor(self, actor): # tell the ASE there is a new server if self.serviceElement: - self.sap_request(addPeer=actor.peer) + self.sap_request(add_actor=actor) - def remove_actor(self, actor): - if _debug: TCPServerDirector._debug("remove_actor %r", actor) + def del_actor(self, actor): + if _debug: TCPServerDirector._debug("del_actor %r", actor) try: del self.servers[actor.peer] except KeyError: - TCPServerDirector._warning("remove_actor: %r not an actor", actor) + TCPServerDirector._warning("del_actor: %r not an actor", actor) # tell the ASE the server has gone away if self.serviceElement: - self.sap_request(delPeer=actor.peer) + self.sap_request(del_actor=actor) + + def actor_error(self, actor, error): + if _debug: TCPServerDirector._debug("actor_error %r %r", actor, error) + + # tell the ASE the actor had an error + if self.serviceElement: + self.sap_request(actor_error=actor, error=error) def get_actor(self, address): """ Get the actor associated with an address or None. """ @@ -733,6 +822,7 @@ def chop(addr): # look for a packet while 1: packet = self.packetFn(buff) + if _debug: StreamToPacket._debug(" - packet: %r", packet) if packet is None: break @@ -786,21 +876,25 @@ def __init__(self, stp, aseID=None, sapID=None): # save a reference to the StreamToPacket object self.stp = stp - def indication(self, addPeer=None, delPeer=None): - if _debug: StreamToPacketSAP._debug("indication addPeer=%r delPeer=%r", addPeer, delPeer) + def indication(self, add_actor=None, del_actor=None, actor_error=None, error=None): + if _debug: StreamToPacketSAP._debug("indication add_actor=%r del_actor=%r", add_actor, del_actor) - if addPeer: + if add_actor: # create empty buffers associated with the peer - self.stp.upstreamBuffer[addPeer] = '' - self.stp.downstreamBuffer[addPeer] = '' + self.stp.upstreamBuffer[add_actor.peer] = '' + self.stp.downstreamBuffer[add_actor.peer] = '' - if delPeer: + if del_actor: # delete the buffer contents associated with the peer - del self.stp.upstreamBuffer[delPeer] - del self.stp.downstreamBuffer[delPeer] + del self.stp.upstreamBuffer[del_actor.peer] + del self.stp.downstreamBuffer[del_actor.peer] # chain this along if self.serviceElement: - self.sap_request(addPeer=addPeer, delPeer=delPeer) + self.sap_request( + add_actor=add_actor, + del_actor=del_actor, + actor_error=actor_error, error=error, + ) bacpypes_debugging(StreamToPacketSAP) diff --git a/py25/bacpypes/udp.py b/py25/bacpypes/udp.py index 6a9ef240..6659a511 100755 --- a/py25/bacpypes/udp.py +++ b/py25/bacpypes/udp.py @@ -43,19 +43,19 @@ def __init__(self, director, peer): # add a timer self.timeout = director.timeout if self.timeout > 0: - self.timer = FunctionTask(self.IdleTimeout) + self.timer = FunctionTask(self.idle_timeout) self.timer.install_task(_time() + self.timeout) else: self.timer = None # tell the director this is a new actor - self.director.AddActor(self) + self.director.add_actor(self) - def IdleTimeout(self): - if _debug: UDPActor._debug("IdleTimeout") + def idle_timeout(self): + if _debug: UDPActor._debug("idle_timeout") # tell the director this is gone - self.director.RemoveActor(self) + self.director.del_actor(self) def indication(self, pdu): if _debug: UDPActor._debug("indication %r", pdu) @@ -77,6 +77,13 @@ def response(self, pdu): # process this as a response from the director self.director.response(pdu) + def handle_error(self, error=None): + if _debug: UDPActor._debug("handle_error %r", error) + + # pass along to the director + if error is not None: + self.director.actor_error(self, error) + bacpypes_debugging(UDPActor) # @@ -157,52 +164,63 @@ def __init__(self, address, timeout=0, reuse=False, actorClass=UDPActor, sid=Non # start with an empty peer pool self.peers = {} - def AddActor(self, actor): + def add_actor(self, actor): """Add an actor when a new one is connected.""" - if _debug: UDPDirector._debug("AddActor %r", actor) + if _debug: UDPDirector._debug("add_actor %r", actor) self.peers[actor.peer] = actor # tell the ASE there is a new client if self.serviceElement: - self.sap_request(addPeer=actor.peer) + self.sap_request(add_actor=actor) - def RemoveActor(self, actor): + def del_actor(self, actor): """Remove an actor when the socket is closed.""" - if _debug: UDPDirector._debug("RemoveActor %r", actor) + if _debug: UDPDirector._debug("del_actor %r", actor) del self.peers[actor.peer] # tell the ASE the client has gone away if self.serviceElement: - self.sap_request(delPeer=actor.peer) + self.sap_request(del_actor=actor) + + def actor_error(self, actor, error): + if _debug: UDPDirector._debug("actor_error %r %r", actor, error) + + # tell the ASE the actor had an error + if self.serviceElement: + self.sap_request(actor_error=actor, error=error) - def GetActor(self, address): + def get_actor(self, address): return self.peers.get(address, None) def handle_connect(self): - if _debug: deferred(UDPDirector._debug, "handle_connect") + if _debug: UDPDirector._debug("handle_connect") def readable(self): return 1 def handle_read(self): - if _debug: deferred(UDPDirector._debug, "handle_read") + if _debug: UDPDirector._debug("handle_read") try: msg, addr = self.socket.recvfrom(65536) - if _debug: deferred(UDPDirector._debug, " - received %d octets from %s", len(msg), addr) + if _debug: UDPDirector._debug(" - received %d octets from %s", len(msg), addr) # send the PDU up to the client deferred(self._response, PDU(msg, source=addr)) except socket.timeout, err: - deferred(UDPDirector._error, "handle_read socket timeout: %s", err) - except OSError, err: + if _debug: UDPDirector._debug(" - socket timeout: %s", err) + + except socket.error, err: if err.args[0] == 11: pass else: - deferred(UDPDirector._error, "handle_read socket error: %s", err) + if _debug: UDPDirector._debug(" - socket error: %s", err) + + # let the director handle the error + self.handle_error(err) def writable(self): """Return true iff there is a request pending.""" @@ -210,24 +228,37 @@ def writable(self): def handle_write(self): """get a PDU from the queue and send it.""" - if _debug: deferred(UDPDirector._debug, "handle_write") + if _debug: UDPDirector._debug("handle_write") try: pdu = self.request.get() sent = self.socket.sendto(pdu.pduData, pdu.pduDestination) - if _debug: deferred(UDPDirector._debug, " - sent %d octets to %s", sent, pdu.pduDestination) + if _debug: UDPDirector._debug(" - sent %d octets to %s", sent, pdu.pduDestination) - except OSError, err: - deferred(UDPDirector._error, "handle_write socket error: %s", err) + except socket.error, err: + if _debug: UDPDirector._debug(" - socket error: %s", err) + + # get the peer + peer = self.peers.get(pdu.pduDestination, None) + if peer: + # let the actor handle the error + peer.handle_error(err) + else: + # let the director handle the error + self.handle_error(err) def handle_close(self): """Remove this from the monitor when it's closed.""" - if _debug: deferred(UDPDirector._debug, "handle_close") + if _debug: UDPDirector._debug("handle_close") self.close() self.socket = None + def handle_error(self, error=None): + """Handle an error...""" + if _debug: UDPDirector._debug("handle_error %r", error) + def indication(self, pdu): """Client requests are queued for delivery.""" if _debug: UDPDirector._debug("indication %r", pdu) diff --git a/py27/bacpypes/__init__.py b/py27/bacpypes/__init__.py index 34cd849c..d388282a 100755 --- a/py27/bacpypes/__init__.py +++ b/py27/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.14.2' +__version__ = '0.15.0' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' @@ -29,6 +29,8 @@ from . import comm from . import task from . import singleton +from . import capability +from . import iocb # # Link Layer Modules @@ -67,6 +69,7 @@ from . import app from . import appservice +from . import service # # Analysis diff --git a/py27/bacpypes/app.py b/py27/bacpypes/app.py index 33e4a364..fe974d64 100755 --- a/py27/bacpypes/app.py +++ b/py27/bacpypes/app.py @@ -4,38 +4,34 @@ Application Module """ +import warnings + from .debugging import bacpypes_debugging, DebugContents, ModuleLogger from .comm import ApplicationServiceElement, bind +from .iocb import IOController, SieveQueue -from .pdu import Address, LocalStation, RemoteStation +from .pdu import Address -from .primitivedata import Atomic, Date, Null, ObjectIdentifier, Time, Unsigned -from .constructeddata import Any, Array, ArrayOf +from .primitivedata import ObjectIdentifier +from .capability import Collector from .appservice import StateMachineAccessPoint, ApplicationServiceAccessPoint from .netservice import NetworkServiceAccessPoint, NetworkServiceElement from .bvllservice import BIPSimple, BIPForeign, AnnexJCodec, UDPMultiplexer -from .object import Property, PropertyError, DeviceObject, \ - registered_object_types, register_object_type -from .apdu import ConfirmedRequestPDU, SimpleAckPDU, RejectPDU, RejectReason -from .apdu import IAmRequest, ReadPropertyACK, Error -from .errors import ExecutionError, \ - RejectException, UnrecognizedService, MissingRequiredParameter, \ - ParameterOutOfRange, \ - AbortException +from .apdu import UnconfirmedRequestPDU, ConfirmedRequestPDU, \ + SimpleAckPDU, ComplexAckPDU, ErrorPDU, RejectPDU, AbortPDU, Error + +from .errors import ExecutionError, UnrecognizedService, AbortException, RejectException # for computing protocol services supported from .apdu import confirmed_request_types, unconfirmed_request_types, \ ConfirmedServiceChoice, UnconfirmedServiceChoice from .basetypes import ServicesSupported -from .apdu import \ - AtomicReadFileACK, \ - AtomicReadFileACKAccessMethodChoice, \ - AtomicReadFileACKAccessMethodRecordAccess, \ - AtomicReadFileACKAccessMethodStreamAccess, \ - AtomicWriteFileACK +# basic services +from .service.device import WhoIsIAmServices +from .service.object import ReadWritePropertyServices # some debugging _debug = 0 @@ -149,7 +145,7 @@ def get_device_info(self, key): def update_device_info(self, info): """The application has updated one or more fields in the device information record and the cache needs to be updated to reflect the - changes. If this is a cached version of a persistent record then this + changes. If this is a cached version of a persistent record then this is the opportunity to update the database.""" if _debug: DeviceInfoCache._debug("update_device_info %r", info) @@ -187,146 +183,53 @@ def release_device_info(self, info): if cache_address is not None: del self.cache[cache_address] -# -# CurrentDateProperty -# - -class CurrentDateProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Date, default=None, optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("{0} is unsubscriptable".format(self.identifier)) - - # get the value - now = Date() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# CurrentTimeProperty -# - -class CurrentTimeProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Time, default=None, optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("{0} is unsubscriptable".format(self.identifier)) - - # get the value - now = Time() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# LocalDeviceObject -# - -@bacpypes_debugging -class LocalDeviceObject(DeviceObject): - - properties = \ - [ CurrentTimeProperty('localTime') - , CurrentDateProperty('localDate') - ] - - defaultProperties = \ - { 'maxApduLengthAccepted': 1024 - , 'segmentationSupported': 'segmentedBoth' - , 'maxSegmentsAccepted': 16 - , 'apduSegmentTimeout': 5000 - , 'apduTimeout': 3000 - , 'numberOfApduRetries': 3 - } - - def __init__(self, **kwargs): - if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) - - # fill in default property values not in kwargs - for attr, value in LocalDeviceObject.defaultProperties.items(): - if attr not in kwargs: - kwargs[attr] = value - - # check for registration - if self.__class__ not in registered_object_types.values(): - if 'vendorIdentifier' not in kwargs: - raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") - register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) - - # check for local time - if 'localDate' in kwargs: - raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") - if 'localTime' in kwargs: - raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") - - # check for a minimum value - if kwargs['maxApduLengthAccepted'] < 50: - raise ValueError("invalid max APDU length accepted") - - # dump the updated attributes - if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) - - # proceed as usual - DeviceObject.__init__(self, **kwargs) - - # create a default implementation of an object list for local devices. - # If it is specified in the kwargs, that overrides this default. - if ('objectList' not in kwargs): - self.objectList = ArrayOf(ObjectIdentifier)([self.objectIdentifier]) - - # if the object has a property list and one wasn't provided - # in the kwargs, then it was created by default and the objectList - # property should be included - if ('propertyList' not in kwargs) and self.propertyList: - # make sure it's not already there - if 'objectList' not in self.propertyList: - self.propertyList.append('objectList') - # # Application # @bacpypes_debugging -class Application(ApplicationServiceElement): +class Application(ApplicationServiceElement, Collector): - def __init__(self, localDevice, localAddress, deviceInfoCache=None, aseID=None): + def __init__(self, localDevice=None, localAddress=None, deviceInfoCache=None, aseID=None): if _debug: Application._debug("__init__ %r %r deviceInfoCache=%r aseID=%r", localDevice, localAddress, deviceInfoCache, aseID) ApplicationServiceElement.__init__(self, aseID) + # local objects by ID and name + self.objectName = {} + self.objectIdentifier = {} + # keep track of the local device - self.localDevice = localDevice + if localDevice: + self.localDevice = localDevice - # use the provided cache or make a default one - if deviceInfoCache: - self.deviceInfoCache = deviceInfoCache - else: - self.deviceInfoCache = DeviceInfoCache() + # bind the device object to this application + localDevice._app = self - # bind the device object to this application - localDevice._app = self + # local objects by ID and name + self.objectName[localDevice.objectName] = localDevice + self.objectIdentifier[localDevice.objectIdentifier] = localDevice - # allow the address to be cast to the correct type - if isinstance(localAddress, Address): - self.localAddress = localAddress - else: - self.localAddress = Address(localAddress) + # local address deprecated, but continue to use the old initializer + if localAddress is not None: + warnings.warn( + "local address at the application layer deprecated", + DeprecationWarning, + ) - # local objects by ID and name - self.objectName = {localDevice.objectName:localDevice} - self.objectIdentifier = {localDevice.objectIdentifier:localDevice} + # allow the address to be cast to the correct type + if isinstance(localAddress, Address): + self.localAddress = localAddress + else: + self.localAddress = Address(localAddress) + + # use the provided cache or make a default one + self.deviceInfoCache = deviceInfoCache or DeviceInfoCache() + + # controllers for managing confirmed requests as a client + self.controllers = {} + + # now set up the rest of the capabilities + Collector.__init__(self) def add_object(self, obj): """Add an object to the local collection.""" @@ -354,8 +257,10 @@ def add_object(self, obj): self.objectName[object_name] = obj self.objectIdentifier[object_identifier] = obj - # append the new object's identifier to the device's object list - self.localDevice.objectList.append(object_identifier) + # append the new object's identifier to the local device's object list + # if there is one and it has an object list property + if self.localDevice and self.localDevice.objectList: + self.localDevice.objectList.append(object_identifier) # let the object know which application stack it belongs to obj._app = self @@ -373,8 +278,10 @@ def delete_object(self, obj): del self.objectIdentifier[object_identifier] # remove the object's identifier from the device's object list - indx = self.localDevice.objectList.index(object_identifier) - del self.localDevice.objectList[indx] + # if there is one and it has an object list property + if self.localDevice and self.localDevice.objectList: + indx = self.localDevice.objectList.index(object_identifier) + del self.localDevice.objectList[indx] # make sure the object knows it's detached from an application obj._app = None @@ -417,6 +324,16 @@ def get_services_supported(self): #----- + def request(self, apdu): + if _debug: Application._debug("request %r", apdu) + + # double check the input is the right kind of APDU + if not isinstance(apdu, (UnconfirmedRequestPDU, ConfirmedRequestPDU)): + raise TypeError("APDU expected") + + # continue + super(Application, self).request(apdu) + def indication(self, apdu): if _debug: Application._debug("indication %r", apdu) @@ -456,345 +373,99 @@ def indication(self, apdu): resp = Error(errorClass='device', errorCode='operationalProblem', context=apdu) self.response(resp) - def do_WhoIsRequest(self, apdu): - """Respond to a Who-Is request.""" - if _debug: Application._debug("do_WhoIsRequest %r", apdu) - - # extract the parameters - low_limit = apdu.deviceInstanceRangeLowLimit - high_limit = apdu.deviceInstanceRangeHighLimit - - # check for consistent parameters - if (low_limit is not None): - if (high_limit is None): - raise MissingRequiredParameter("deviceInstanceRangeHighLimit required") - if (low_limit < 0) or (low_limit > 4194303): - raise ParameterOutOfRange("deviceInstanceRangeLowLimit out of range") - if (high_limit is not None): - if (low_limit is None): - raise MissingRequiredParameter("deviceInstanceRangeLowLimit required") - if (high_limit < 0) or (high_limit > 4194303): - raise ParameterOutOfRange("deviceInstanceRangeHighLimit out of range") - - # see we should respond - if (low_limit is not None): - if (self.localDevice.objectIdentifier[1] < low_limit): - return - if (high_limit is not None): - if (self.localDevice.objectIdentifier[1] > high_limit): - return +# +# ApplicationIOController +# - # create a I-Am "response" back to the source - iAm = IAmRequest() - iAm.pduDestination = apdu.pduSource - iAm.iAmDeviceIdentifier = self.localDevice.objectIdentifier - iAm.maxAPDULengthAccepted = self.localDevice.maxApduLengthAccepted - iAm.segmentationSupported = self.localDevice.segmentationSupported - iAm.vendorID = self.localDevice.vendorIdentifier - if _debug: Application._debug(" - iAm: %r", iAm) +@bacpypes_debugging +class ApplicationIOController(IOController, Application): - # away it goes - self.request(iAm) + def __init__(self, *args, **kwargs): + if _debug: ApplicationIOController._debug("__init__") + IOController.__init__(self) + Application.__init__(self, *args, **kwargs) - def do_IAmRequest(self, apdu): - """Respond to an I-Am request.""" - if _debug: Application._debug("do_IAmRequest %r", apdu) + # queues for each address + self.queue_by_address = {} - def do_ReadPropertyRequest(self, apdu): - """Return the value of some property of one of our objects.""" - if _debug: Application._debug("do_ReadPropertyRequest %r", apdu) + def process_io(self, iocb): + if _debug: ApplicationIOController._debug("process_io %r", iocb) - # extract the object identifier - objId = apdu.objectIdentifier + # get the destination address from the pdu + destination_address = iocb.args[0].pduDestination + if _debug: ApplicationIOController._debug(" - destination_address: %r", destination_address) - # check for wildcard - if (objId == ('device', 4194303)): - if _debug: Application._debug(" - wildcard device identifier") - objId = self.localDevice.objectIdentifier + # look up the queue + queue = self.queue_by_address.get(destination_address, None) + if not queue: + queue = SieveQueue(self.request, destination_address) + self.queue_by_address[destination_address] = queue + if _debug: ApplicationIOController._debug(" - queue: %r", queue) - # get the object - obj = self.get_object_id(objId) - if _debug: Application._debug(" - object: %r", obj) + # ask the queue to process the request + queue.request_io(iocb) - if not obj: - resp = Error(errorClass='object', errorCode='unknownObject', context=apdu) - else: - try: - # get the datatype - datatype = obj.get_datatype(apdu.propertyIdentifier) - if _debug: Application._debug(" - datatype: %r", datatype) - - # get the value - value = obj.ReadProperty(apdu.propertyIdentifier, apdu.propertyArrayIndex) - if _debug: Application._debug(" - value: %r", value) - if value is None: - raise PropertyError(apdu.propertyIdentifier) - - # change atomic values into something encodeable - if issubclass(datatype, Atomic): - value = datatype(value) - elif issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): - if apdu.propertyArrayIndex == 0: - value = Unsigned(value) - elif issubclass(datatype.subtype, Atomic): - value = datatype.subtype(value) - elif not isinstance(value, datatype.subtype): - raise TypeError("invalid result datatype, expecting {0} and got {1}" \ - .format(datatype.subtype.__name__, type(value).__name__)) - elif not isinstance(value, datatype): - raise TypeError("invalid result datatype, expecting {0} and got {1}" \ - .format(datatype.__name__, type(value).__name__)) - if _debug: Application._debug(" - encodeable value: %r", value) - - # this is a ReadProperty ack - resp = ReadPropertyACK(context=apdu) - resp.objectIdentifier = objId - resp.propertyIdentifier = apdu.propertyIdentifier - resp.propertyArrayIndex = apdu.propertyArrayIndex - - # save the result in the property value - resp.propertyValue = Any() - resp.propertyValue.cast_in(value) - - except PropertyError: - resp = Error(errorClass='object', errorCode='unknownProperty', context=apdu) - if _debug: Application._debug(" - resp: %r", resp) - - # return the result - self.response(resp) - - def do_WritePropertyRequest(self, apdu): - """Change the value of some property of one of our objects.""" - if _debug: Application._debug("do_WritePropertyRequest %r", apdu) - - # get the object - obj = self.get_object_id(apdu.objectIdentifier) - if _debug: Application._debug(" - object: %r", obj) - - if not obj: - resp = Error(errorClass='object', errorCode='unknownObject', context=apdu) - else: - try: - # check if the property exists - if obj.ReadProperty(apdu.propertyIdentifier, apdu.propertyArrayIndex) is None: - raise PropertyError(apdu.propertyIdentifier) - - # get the datatype, special case for null - if apdu.propertyValue.is_application_class_null(): - datatype = Null - else: - datatype = obj.get_datatype(apdu.propertyIdentifier) - if _debug: Application._debug(" - datatype: %r", datatype) - - # special case for array parts, others are managed by cast_out - if issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): - if apdu.propertyArrayIndex == 0: - value = apdu.propertyValue.cast_out(Unsigned) - else: - value = apdu.propertyValue.cast_out(datatype.subtype) - else: - value = apdu.propertyValue.cast_out(datatype) - if _debug: Application._debug(" - value: %r", value) - - # change the value - value = obj.WriteProperty(apdu.propertyIdentifier, value, apdu.propertyArrayIndex, apdu.priority) - - # success - resp = SimpleAckPDU(context=apdu) - - except PropertyError: - resp = Error(errorClass='object', errorCode='unknownProperty', context=apdu) - if _debug: Application._debug(" - resp: %r", resp) - - # return the result - self.response(resp) - - def do_AtomicReadFileRequest(self, apdu): - """Return one of our records.""" - if _debug: Application._debug("do_AtomicReadFileRequest %r", apdu) - - if (apdu.fileIdentifier[0] != 'file'): - resp = Error(errorClass='services', errorCode='inconsistentObjectType', context=apdu) - if _debug: Application._debug(" - error resp: %r", resp) - self.response(resp) - return + def _app_complete(self, address, apdu): + if _debug: ApplicationIOController._debug("_app_complete %r %r", address, apdu) - # get the object - obj = self.get_object_id(apdu.fileIdentifier) - if _debug: Application._debug(" - object: %r", obj) - - if not obj: - resp = Error(errorClass='object', errorCode='unknownObject', context=apdu) - elif apdu.accessMethod.recordAccess: - # check against the object - if obj.fileAccessMethod != 'recordAccess': - resp = Error(errorClass='services', - errorCode='invalidFileAccessMethod', - context=apdu - ) - ### verify start is valid - double check this (empty files?) - elif (apdu.accessMethod.recordAccess.fileStartRecord < 0) or \ - (apdu.accessMethod.recordAccess.fileStartRecord >= len(obj)): - resp = Error(errorClass='services', - errorCode='invalidFileStartPosition', - context=apdu - ) - else: - # pass along to the object - end_of_file, record_data = obj.ReadFile( - apdu.accessMethod.recordAccess.fileStartRecord, - apdu.accessMethod.recordAccess.requestedRecordCount, - ) - if _debug: Application._debug(" - record_data: %r", record_data) - - # this is an ack - resp = AtomicReadFileACK(context=apdu, - endOfFile=end_of_file, - accessMethod=AtomicReadFileACKAccessMethodChoice( - recordAccess=AtomicReadFileACKAccessMethodRecordAccess( - fileStartRecord=apdu.accessMethod.recordAccess.fileStartRecord, - returnedRecordCount=len(record_data), - fileRecordData=record_data, - ), - ), - ) - - elif apdu.accessMethod.streamAccess: - # check against the object - if obj.fileAccessMethod != 'streamAccess': - resp = Error(errorClass='services', - errorCode='invalidFileAccessMethod', - context=apdu - ) - ### verify start is valid - double check this (empty files?) - elif (apdu.accessMethod.streamAccess.fileStartPosition < 0) or \ - (apdu.accessMethod.streamAccess.fileStartPosition >= len(obj)): - resp = Error(errorClass='services', - errorCode='invalidFileStartPosition', - context=apdu - ) - else: - # pass along to the object - end_of_file, record_data = obj.ReadFile( - apdu.accessMethod.streamAccess.fileStartPosition, - apdu.accessMethod.streamAccess.requestedOctetCount, - ) - if _debug: Application._debug(" - record_data: %r", record_data) - - # this is an ack - resp = AtomicReadFileACK(context=apdu, - endOfFile=end_of_file, - accessMethod=AtomicReadFileACKAccessMethodChoice( - streamAccess=AtomicReadFileACKAccessMethodStreamAccess( - fileStartPosition=apdu.accessMethod.streamAccess.fileStartPosition, - fileData=record_data, - ), - ), - ) - - if _debug: Application._debug(" - resp: %r", resp) - - # return the result - self.response(resp) - - def do_AtomicWriteFileRequest(self, apdu): - """Return one of our records.""" - if _debug: Application._debug("do_AtomicWriteFileRequest %r", apdu) - - if (apdu.fileIdentifier[0] != 'file'): - resp = Error(errorClass='services', errorCode='inconsistentObjectType', context=apdu) - if _debug: Application._debug(" - error resp: %r", resp) - self.response(resp) + # look up the queue + queue = self.queue_by_address.get(address, None) + if not queue: + ApplicationIOController._debug("no queue for %r" % (address,)) return + if _debug: ApplicationIOController._debug(" - queue: %r", queue) - # get the object - obj = self.get_object_id(apdu.fileIdentifier) - if _debug: Application._debug(" - object: %r", obj) - - if not obj: - resp = Error(errorClass='object', errorCode='unknownObject', context=apdu) - elif apdu.accessMethod.recordAccess: - # check against the object - if obj.fileAccessMethod != 'recordAccess': - resp = Error(errorClass='services', - errorCode='invalidFileAccessMethod', - context=apdu - ) - if _debug: Application._debug(" - error resp: %r", resp) - self.response(resp) - return - - # check for read-only - if obj.readOnly: - resp = Error(errorClass='services', - errorCode='fileAccessDenied', - context=apdu - ) - if _debug: Application._debug(" - error resp: %r", resp) - self.response(resp) - return - - # pass along to the object - start_record = obj.WriteFile( - apdu.accessMethod.recordAccess.fileStartRecord, - apdu.accessMethod.recordAccess.recordCount, - apdu.accessMethod.recordAccess.fileRecordData, - ) - if _debug: Application._debug(" - start_record: %r", start_record) + # make sure it has an active iocb + if not queue.active_iocb: + ApplicationIOController._debug("no active request for %r" % (address,)) + return - # this is an ack - resp = AtomicWriteFileACK(context=apdu, - fileStartRecord=start_record, - ) + # this request is complete + if isinstance(apdu, (None.__class__, SimpleAckPDU, ComplexAckPDU)): + queue.complete_io(queue.active_iocb, apdu) + elif isinstance(apdu, (ErrorPDU, RejectPDU, AbortPDU)): + queue.abort_io(queue.active_iocb, apdu) + else: + raise RuntimeError("unrecognized APDU type") + if _debug: Application._debug(" - controller finished") - elif apdu.accessMethod.streamAccess: - # check against the object - if obj.fileAccessMethod != 'streamAccess': - resp = Error(errorClass='services', - errorCode='invalidFileAccessMethod', - context=apdu - ) - if _debug: Application._debug(" - error resp: %r", resp) - self.response(resp) - return + # if the queue is empty and idle, forget about the controller + if not queue.ioQueue.queue and not queue.active_iocb: + if _debug: ApplicationIOController._debug(" - queue is empty") + del self.queue_by_address[address] - # check for read-only - if obj.readOnly: - resp = Error(errorClass='services', - errorCode='fileAccessDenied', - context=apdu - ) - if _debug: Application._debug(" - error resp: %r", resp) - self.response(resp) - return + def request(self, apdu): + if _debug: ApplicationIOController._debug("request %r", apdu) - # pass along to the object - start_position = obj.WriteFile( - apdu.accessMethod.streamAccess.fileStartPosition, - apdu.accessMethod.streamAccess.fileData, - ) - if _debug: Application._debug(" - start_position: %r", start_position) + # send it downstream + super(ApplicationIOController, self).request(apdu) - # this is an ack - resp = AtomicWriteFileACK(context=apdu, - fileStartPosition=start_position, - ) + # if this was an unconfirmed request, it's complete, no message + if isinstance(apdu, UnconfirmedRequestPDU): + self._app_complete(apdu.pduDestination, None) - if _debug: Application._debug(" - resp: %r", resp) + def confirmation(self, apdu): + if _debug: ApplicationIOController._debug("confirmation %r", apdu) - # return the result - self.response(resp) + # this is an ack, error, reject or abort + self._app_complete(apdu.pduSource, apdu) # # BIPSimpleApplication # @bacpypes_debugging -class BIPSimpleApplication(Application): +class BIPSimpleApplication(ApplicationIOController, WhoIsIAmServices, ReadWritePropertyServices): def __init__(self, localDevice, localAddress, deviceInfoCache=None, aseID=None): if _debug: BIPSimpleApplication._debug("__init__ %r %r deviceInfoCache=%r aseID=%r", localDevice, localAddress, deviceInfoCache, aseID) - Application.__init__(self, localDevice, localAddress, deviceInfoCache, aseID) + ApplicationIOController.__init__(self, localDevice, deviceInfoCache, aseID=aseID) + + # local address might be useful for subclasses + if isinstance(localAddress, Address): + self.localAddress = localAddress + else: + self.localAddress = Address(localAddress) # include a application decoder self.asap = ApplicationServiceAccessPoint() @@ -834,11 +505,17 @@ def __init__(self, localDevice, localAddress, deviceInfoCache=None, aseID=None): # @bacpypes_debugging -class BIPForeignApplication(Application): +class BIPForeignApplication(ApplicationIOController, WhoIsIAmServices, ReadWritePropertyServices): def __init__(self, localDevice, localAddress, bbmdAddress, bbmdTTL, aseID=None): if _debug: BIPForeignApplication._debug("__init__ %r %r %r %r aseID=%r", localDevice, localAddress, bbmdAddress, bbmdTTL, aseID) - Application.__init__(self, localDevice, localAddress, aseID) + ApplicationIOController.__init__(self, localDevice, aseID=aseID) + + # local address might be useful for subclasses + if isinstance(localAddress, Address): + self.localAddress = localAddress + else: + self.localAddress = Address(localAddress) # include a application decoder self.asap = ApplicationServiceAccessPoint() @@ -903,4 +580,3 @@ def __init__(self, localAddress, eID=None): # bind the NSAP to the stack, no network number self.nsap.bind(self.bip) - diff --git a/py27/bacpypes/appservice.py b/py27/bacpypes/appservice.py index 77b6f0de..f11edd17 100755 --- a/py27/bacpypes/appservice.py +++ b/py27/bacpypes/appservice.py @@ -644,7 +644,7 @@ def segmented_confirmation_timeout(self): class ServerSSM(SSM): def __init__(self, sap, remoteDevice): - if _debug: ServerSSM._debug("__init__ %s %r %r", sap, remoteDevice) + if _debug: ServerSSM._debug("__init__ %s %r", sap, remoteDevice) SSM.__init__(self, sap, remoteDevice) def set_state(self, newState, timer=0): @@ -1061,8 +1061,7 @@ def __init__(self, localDevice=None, deviceInfoCache=None, sap=None, cid=None): Client.__init__(self, cid) ServiceAccessPoint.__init__(self, sap) - # save a reference to the local device object and the cache - self.localDevice = localDevice + # save a reference to the device information cache self.deviceInfoCache = deviceInfoCache # client settings diff --git a/py27/bacpypes/capability.py b/py27/bacpypes/capability.py new file mode 100644 index 00000000..2aa18537 --- /dev/null +++ b/py27/bacpypes/capability.py @@ -0,0 +1,151 @@ +#!/usr/bin/python + +""" +Capability +""" + +from .debugging import bacpypes_debugging, ModuleLogger + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# Capability +# + +@bacpypes_debugging +class Capability(object): + + _zindex = 99 + + def __init__(self): + if _debug: Capability._debug("__init__") + +# +# Collector +# + +@bacpypes_debugging +class Collector(object): + + def __init__(self): + if _debug: Collector._debug("__init__ (%r %r)", self.__class__, self.__class__.__bases__) + + # gather the capbilities + self.capabilities = self._search_capability(self.__class__) + + # give them a chance to init + for cls in self.capabilities: + if hasattr(cls, '__init__') and cls is not Collector: + if _debug: Collector._debug(" - calling %r.__init__", cls) + cls.__init__(self) + + def _search_capability(self, base): + """Given a class, return a list of all of the derived classes that + are themselves derived from Capability.""" + if _debug: Collector._debug("_search_capability %r", base) + + rslt = [] + for cls in base.__bases__: + if issubclass(cls, Collector): + map( rslt.append, self._search_capability(cls)) + elif issubclass(cls, Capability): + rslt.append(cls) + if _debug: Collector._debug(" - rslt: %r", rslt) + + return rslt + + def capability_functions(self, fn): + """This generator yields functions that match the + requested capability sorted by z-index.""" + if _debug: Collector._debug("capability_functions %r", fn) + + # build a list of functions to call + fns = [] + for cls in self.capabilities: + xfn = getattr(cls, fn, None) + if _debug: Collector._debug(" - cls, xfn: %r, %r", cls, xfn) + if xfn: + fns.append( (getattr(cls, '_zindex', None), xfn) ) + + # sort them by z-index + fns.sort(key=lambda v: v[0]) + if _debug: Collector._debug(" - fns: %r", fns) + + # now yield them in order + for xindx, xfn in fns: + if _debug: Collector._debug(" - yield xfn: %r", xfn) + yield xfn + + def add_capability(self, cls): + """Add a capability to this object.""" + if _debug: Collector._debug("add_capability %r", cls) + + # the new type has everything the current one has plus this new one + bases = (self.__class__, cls) + if _debug: Collector._debug(" - bases: %r", bases) + + # save this additional class + self.capabilities.append(cls) + + # morph into a new type + newtype = type(self.__class__.__name__ + '+' + cls.__name__, bases, {}) + self.__class__ = newtype + + # allow the new type to init + if hasattr(cls, '__init__'): + if _debug: Collector._debug(" - calling %r.__init__", cls) + cls.__init__(self) + +# +# compose_capability +# + +@bacpypes_debugging +def compose_capability(base, *classes): + """Create a new class starting with the base and adding capabilities.""" + if _debug: compose_capability._debug("compose_capability %r %r", base, classes) + + # make sure the base is a Collector + if not issubclass(base, Collector): + raise TypeError("base must be a subclass of Collector") + + # make sure you only add capabilities + for cls in classes: + if not issubclass(cls, Capability): + raise TypeError("%s is not a Capability subclass" % (cls,)) + + # start with everything the base has and add the new ones + bases = (base,) + classes + + # build a new name + name = base.__name__ + for cls in classes: + name += '+' + cls.__name__ + + # return a new type + return type(name, bases, {}) + +# +# add_capability +# + +@bacpypes_debugging +def add_capability(base, *classes): + """Add capabilites to an existing base, all objects get the additional + functionality, but don't get inited. Use with great care!""" + if _debug: add_capability._debug("add_capability %r %r", base, classes) + + # start out with a collector + if not issubclass(base, Collector): + raise TypeError("base must be a subclass of Collector") + + # make sure you only add capabilities + for cls in classes: + if not issubclass(cls, Capability): + raise TypeError("%s is not a Capability subclass" % (cls,)) + + base.__bases__ += classes + for cls in classes: + base.__name__ += '+' + cls.__name__ diff --git a/py27/bacpypes/comm.py b/py27/bacpypes/comm.py index 993ff4e7..dfef7aae 100755 --- a/py27/bacpypes/comm.py +++ b/py27/bacpypes/comm.py @@ -108,12 +108,12 @@ def __init__(self, data=None, *args, **kwargs): super(PDUData, self).__init__(*args, **kwargs) # function acts like a copy constructor - if isinstance(data, PDUData) or isinstance(data, PDU): - self.pduData = _copy(data.pduData) - elif data is None: + if data is None: self.pduData = b'' elif isinstance(data, str): self.pduData = data + elif isinstance(data, PDUData) or isinstance(data, PDU): + self.pduData = _copy(data.pduData) else: raise TypeError("string expected") @@ -197,7 +197,7 @@ def dict_contents(self, use_dict=None, as_class=dict): @bacpypes_debugging class PDU(PCI, PDUData): - def __init__(self, data='', **kwargs): + def __init__(self, data=None, **kwargs): if _debug: PDU._debug("__init__ %r %r", data, kwargs) # pick up some optional kwargs @@ -226,7 +226,7 @@ def __str__(self): def dict_contents(self, use_dict=None, as_class=dict): """Return the contents of an object as a dict.""" - if _debug: PDUData._debug("dict_contents use_dict=%r as_class=%r", use_dict, as_class) + if _debug: PDU._debug("dict_contents use_dict=%r as_class=%r", use_dict, as_class) # make/extend the dictionary of content if use_dict is None: diff --git a/py27/bacpypes/console.py b/py27/bacpypes/console.py index 5fea3719..70584100 100755 --- a/py27/bacpypes/console.py +++ b/py27/bacpypes/console.py @@ -50,14 +50,23 @@ def writable(self): def handle_read(self): if _debug: deferred(ConsoleClient._debug, "handle_read") - data = sys.stdin.read() + + # read from stdin and encode it + data = sys.stdin.read().encode('utf-8') if _debug: deferred(ConsoleClient._debug, " - data: %r", data) + + # make a PDU and send it downstream deferred(self.request, PDU(data)) def confirmation(self, pdu): if _debug: deferred(ConsoleClient._debug, "confirmation %r", pdu) try: - sys.stdout.write(pdu.pduData) + # decode the data + data = pdu.pduData.decode('utf-8') + if _debug: deferred(ConsoleClient._debug, " - data: %r", data) + + # send it out + sys.stdout.write(data) except Exception as err: ConsoleClient._exception("Confirmation sys.stdout.write exception: %r", err) @@ -81,13 +90,22 @@ def writable(self): def handle_read(self): if _debug: deferred(ConsoleServer._debug, "handle_read") - data = sys.stdin.read() + + # read from stdin and encode it + data = sys.stdin.read().encode('utf-8') if _debug: deferred(ConsoleServer._debug, " - data: %r", data) + + # make a PDU and send it upstream deferred(self.response, PDU(data)) def indication(self, pdu): - if _debug: deferred(ConsoleServer._debug, "Indication %r", pdu) + if _debug: deferred(ConsoleServer._debug, "indication %r", pdu) try: - sys.stdout.write(pdu.pduData) + # decode the data + data = pdu.pduData.decode('utf-8') + if _debug: deferred(ConsoleServer._debug, " - data: %r", data) + + # send it out + sys.stdout.write(data) except Exception as err: - ConsoleServer._exception("Indication sys.stdout.write exception: %r", err) + ConsoleServer._exception("indication sys.stdout.write exception: %r", err) diff --git a/py27/bacpypes/consolecmd.py b/py27/bacpypes/consolecmd.py index 27caf515..e4b33043 100755 --- a/py27/bacpypes/consolecmd.py +++ b/py27/bacpypes/consolecmd.py @@ -258,7 +258,7 @@ def do_help(self, args): 'help' or '?' with no arguments prints a list of commands for which help is available 'help ' or '? ' gives help on """ - if _debug: ConsoleCmd._debug("do_exit %r", args) + if _debug: ConsoleCmd._debug("do_help %r", args) # the only reason to define this method is for the help text in the doc string cmd.Cmd.do_help(self, args) diff --git a/py27/bacpypes/consolelogging.py b/py27/bacpypes/consolelogging.py index 8d095a3f..e8301ed1 100755 --- a/py27/bacpypes/consolelogging.py +++ b/py27/bacpypes/consolelogging.py @@ -28,7 +28,7 @@ # def ConsoleLogHandler(loggerRef='', handler=None, level=logging.DEBUG, color=None): - """Add a handler with our custom formatter to a logger.""" + """Add a handler to stderr with our custom formatter to a logger.""" if isinstance(loggerRef, logging.Logger): pass diff --git a/py27/bacpypes/core.py b/py27/bacpypes/core.py index 048d9a86..00b4a16e 100755 --- a/py27/bacpypes/core.py +++ b/py27/bacpypes/core.py @@ -79,7 +79,7 @@ def run(spin=SPIN): # call the functions for fn, args, kwargs in fnlist: - # if _debug: run._debug(" - call: %r %r %r", fn, args, kwargs) +# if _debug: run._debug(" - call: %r %r %r", fn, args, kwargs) fn( *args, **kwargs) # done with this list @@ -210,13 +210,22 @@ def print_stack(sig, frame): # deferred # +@bacpypes_debugging def deferred(fn, *args, **kwargs): - # _log.debug("deferred %r %r %r", fn, args, kwargs) - global deferredFns +# if _debug: +# deferred._debug("deferred %r %r %r", fn, args, kwargs) +# for filename, lineno, _, _ in traceback.extract_stack()[-6:-1]: +# deferred._debug(" %s:%s" % (filename.split('/')[-1], lineno)) + global deferredFns, taskManager # append it to the list deferredFns.append((fn, args, kwargs)) + # trigger the task manager event + if taskManager and taskManager.trigger: +# if _debug: deferred._debug(" - trigger") + taskManager.trigger.set() + # # enable_sleeping # diff --git a/py27/bacpypes/debugging.py b/py27/bacpypes/debugging.py index 93a6378a..0e79a66c 100755 --- a/py27/bacpypes/debugging.py +++ b/py27/bacpypes/debugging.py @@ -65,7 +65,7 @@ def ModuleLogger(globs): that aren't necessary. """ # make sure that _debug is defined - if not globs.has_key('_debug'): + if '_debug' not in globs: raise RuntimeError("define _debug before creating a module logger") # create a logger to be assigned to _log diff --git a/py27/bacpypes/errors.py b/py27/bacpypes/errors.py index 7c1dcba5..bb25871b 100755 --- a/py27/bacpypes/errors.py +++ b/py27/bacpypes/errors.py @@ -100,7 +100,7 @@ class InconsistentParameters(RejectException): conditional service argument that should not be present. This condition could also elicit a Reject PDU with a Reject Reason of INVALID_TAG. """ - + rejectReason = 'inconsistentParameters' diff --git a/py27/bacpypes/iocb.py b/py27/bacpypes/iocb.py new file mode 100644 index 00000000..1b02bc8a --- /dev/null +++ b/py27/bacpypes/iocb.py @@ -0,0 +1,1002 @@ +#!/usr/bin/python + +""" +IOCB Module +""" + +import sys +import logging +from time import time as _time + +import threading +from bisect import bisect_left + +from .debugging import bacpypes_debugging, ModuleLogger, DebugContents + +from .core import deferred +from .task import FunctionTask +from .comm import Client + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) +_statelog = logging.getLogger(__name__ + "._statelog") + +# globals +local_controllers = {} + +# +# IOCB States +# + +IDLE = 0 # has not been submitted +PENDING = 1 # queued, waiting for processing +ACTIVE = 2 # being processed +COMPLETED = 3 # finished +ABORTED = 4 # finished in a bad way + +_stateNames = { + 0: 'IDLE', + 1: 'PENDING', + 2: 'ACTIVE', + 3: 'COMPLETED', + 4: 'ABORTED', + } + +# +# IOQController States +# + +CTRL_IDLE = 0 # nothing happening +CTRL_ACTIVE = 1 # working on an iocb +CTRL_WAITING = 1 # waiting between iocb requests (throttled) + +_ctrlStateNames = { + 0: 'IDLE', + 1: 'ACTIVE', + 2: 'WAITING', + } + +# special abort error +TimeoutError = RuntimeError("timeout") + +# current time formatting (short version) +_strftime = lambda: "%011.6f" % (_time() % 3600,) + +# +# IOCB - Input Output Control Block +# + +_identNext = 1 +_identLock = threading.Lock() + +@bacpypes_debugging +class IOCB(DebugContents): + + _debugContents = \ + ( 'args', 'kwargs' + , 'ioState', 'ioResponse-', 'ioError' + , 'ioController', 'ioServerRef', 'ioControllerRef', 'ioClientID', 'ioClientAddr' + , 'ioComplete', 'ioCallback+', 'ioQueue', 'ioPriority', 'ioTimeout' + ) + + def __init__(self, *args, **kwargs): + global _identNext + + # lock the identity sequence number + _identLock.acquire() + + # generate a unique identity for this block + ioID = _identNext + _identNext += 1 + + # release the lock + _identLock.release() + + # debugging postponed until ID acquired + if _debug: IOCB._debug("__init__(%d) %r %r", ioID, args, kwargs) + + # save the ID + self.ioID = ioID + + # save the request parameters + self.args = args + self.kwargs = kwargs + + # start with an idle request + self.ioState = IDLE + self.ioResponse = None + self.ioError = None + + # blocks are bound to a controller + self.ioController = None + + # each block gets a completion event + self.ioComplete = threading.Event() + self.ioComplete.clear() + + # applications can set a callback functions + self.ioCallback = [] + + # request is not currently queued + self.ioQueue = None + + # extract the priority if it was given + self.ioPriority = kwargs.get('_priority', 0) + if '_priority' in kwargs: + if _debug: IOCB._debug(" - ioPriority: %r", self.ioPriority) + del kwargs['_priority'] + + # request has no timeout + self.ioTimeout = None + + def add_callback(self, fn, *args, **kwargs): + """Pass a function to be called when IO is complete.""" + if _debug: IOCB._debug("add_callback(%d) %r %r %r", self.ioID, fn, args, kwargs) + + # store it + self.ioCallback.append((fn, args, kwargs)) + + # already complete? + if self.ioComplete.isSet(): + self.trigger() + + def wait(self, *args): + """Wait for the completion event to be set.""" + if _debug: IOCB._debug("wait(%d) %r", self.ioID, args) + + # waiting from a non-daemon thread could be trouble + self.ioComplete.wait(*args) + + def trigger(self): + """Set the completion event and make the callback(s).""" + if _debug: IOCB._debug("trigger(%d)", self.ioID) + + # if it's queued, remove it from its queue + if self.ioQueue: + if _debug: IOCB._debug(" - dequeue") + self.ioQueue.Remove(self) + + # if there's a timer, cancel it + if self.ioTimeout: + if _debug: IOCB._debug(" - cancel timeout") + self.ioTimeout.SuspendTask() + + # set the completion event + self.ioComplete.set() + + # make the callback(s) + for fn, args, kwargs in self.ioCallback: + if _debug: IOCB._debug(" - callback fn: %r %r %r", fn, args, kwargs) + fn(self, *args, **kwargs) + + def complete(self, msg): + """Called to complete a transaction, usually when ProcessIO has + shipped the IOCB off to some other thread or function.""" + if _debug: IOCB._debug("complete(%d) %r", self.ioID, msg) + + if self.ioController: + # pass to controller + self.ioController.complete_io(self, msg) + else: + # just fill in the data + self.ioState = COMPLETED + self.ioResponse = msg + self.trigger() + + def abort(self, err): + """Called by a client to abort a transaction.""" + if _debug: IOCB._debug("abort(%d) %r", self.ioID, err) + + if self.ioController: + # pass to controller + self.ioController.abort_io(self, err) + elif self.ioState < COMPLETED: + # just fill in the data + self.ioState = ABORTED + self.ioError = err + self.trigger() + + def set_timeout(self, delay, err=TimeoutError): + """Called to set a transaction timer.""" + if _debug: IOCB._debug("set_timeout(%d) %r err=%r", self.ioID, delay, err) + + # if one has already been created, cancel it + if self.ioTimeout: + self.ioTimeout.suspend_task() + else: + self.ioTimeout = FunctionTask(self.Abort, err) + + # (re)schedule it + self.ioTimeout.install_task(_time() + delay) + + def __repr__(self): + xid = id(self) + if (xid < 0): xid += (1 << 32) + + sname = self.__module__ + '.' + self.__class__.__name__ + desc = "(%d)" % (self.ioID) + + return '<' + sname + desc + ' instance at 0x%08x' % (xid,) + '>' + +# +# IOChainMixIn +# + +@bacpypes_debugging +class IOChainMixIn(DebugContents): + + _debugContents = ( 'ioChain++', ) + + def __init__(self, iocb): + if _debug: IOChainMixIn._debug("__init__ %r", iocb) + + # save a refence back to the iocb + self.ioChain = iocb + + # set the callback to follow the chain + self.add_callback(self.chain_callback) + + # if we're not chained, there's no notification to do + if not self.ioChain: + return + + # this object becomes its controller + iocb.ioController = self + + # consider the parent active + iocb.ioState = ACTIVE + + try: + if _debug: IOChainMixIn._debug(" - encoding") + + # let the derived class set the args and kwargs + self.encode() + + if _debug: IOChainMixIn._debug(" - encode complete") + except: + # extract the error and abort the request + err = sys.exc_info()[1] + if _debug: IOChainMixIn._exception(" - encoding exception: %r", err) + + iocb.abort(err) + + def chain_callback(self, iocb): + """Callback when this iocb completes.""" + if _debug: IOChainMixIn._debug("chain_callback %r", iocb) + + # if we're not chained, there's no notification to do + if not self.ioChain: + return + + # refer to the chained iocb + iocb = self.ioChain + + try: + if _debug: IOChainMixIn._debug(" - decoding") + + # let the derived class transform the data + self.decode() + + if _debug: IOChainMixIn._debug(" - decode complete") + except: + # extract the error and abort + err = sys.exc_info()[1] + if _debug: IOChainMixIn._exception(" - decoding exception: %r", err) + + iocb.ioState = ABORTED + iocb.ioError = err + + # break the references + self.ioChain = None + iocb.ioController = None + + # notify the client + iocb.trigger() + + def abort_io(self, iocb, err): + """Forward the abort downstream.""" + if _debug: IOChainMixIn._debug("abort_io %r %r", iocb, err) + + # make sure we're being notified of an abort request from + # the iocb we are chained from + if iocb is not self.ioChain: + raise RuntimeError("broken chain") + + # call my own Abort(), which may forward it to a controller or + # be overridden by IOGroup + self.abort(err) + + def encode(self): + """Hook to transform the request, called when this IOCB is + chained.""" + if _debug: IOChainMixIn._debug("encode") + + # by default do nothing, the arguments have already been supplied + + def decode(self): + """Hook to transform the response, called when this IOCB is + completed.""" + if _debug: IOChainMixIn._debug("decode") + + # refer to the chained iocb + iocb = self.ioChain + + # if this has completed successfully, pass it up + if self.ioState == COMPLETED: + if _debug: IOChainMixIn._debug(" - completed: %r", self.ioResponse) + + # change the state and transform the content + iocb.ioState = COMPLETED + iocb.ioResponse = self.ioResponse + + # if this aborted, pass that up too + elif self.ioState == ABORTED: + if _debug: IOChainMixIn._debug(" - aborted: %r", self.ioError) + + # change the state + iocb.ioState = ABORTED + iocb.ioError = self.ioError + + else: + raise RuntimeError("invalid state: %d" % (self.ioState,)) + +# +# IOChain +# + +@bacpypes_debugging +class IOChain(IOCB, IOChainMixIn): + + def __init__(self, chain, *args, **kwargs): + """Initialize a chained control block.""" + if _debug: IOChain._debug("__init__ %r %r %r", chain, args, kwargs) + + # initialize IOCB part to pick up the ioID + IOCB.__init__(self, *args, **kwargs) + IOChainMixIn.__init__(self, chain) + +# +# IOGroup +# + +@bacpypes_debugging +class IOGroup(IOCB, DebugContents): + + _debugContents = ('ioMembers',) + + def __init__(self): + """Initialize a group.""" + if _debug: IOGroup._debug("__init__") + IOCB.__init__(self) + + # start with an empty list of members + self.ioMembers = [] + + # start out being done. When an IOCB is added to the + # group that is not already completed, this state will + # change to PENDING. + self.ioState = COMPLETED + self.ioComplete.set() + + def add(self, iocb): + """Add an IOCB to the group, you can also add other groups.""" + if _debug: IOGroup._debug("add %r", iocb) + + # add this to our members + self.ioMembers.append(iocb) + + # assume all of our members have not completed yet + self.ioState = PENDING + self.ioComplete.clear() + + # when this completes, call back to the group. If this + # has already completed, it will trigger + iocb.add_callback(self.group_callback) + + def group_callback(self, iocb): + """Callback when a child iocb completes.""" + if _debug: IOGroup._debug("group_callback %r", iocb) + + # check all the members + for iocb in self.ioMembers: + if not iocb.ioComplete.isSet(): + if _debug: IOGroup._debug(" - waiting for child: %r", iocb) + break + else: + if _debug: IOGroup._debug(" - all children complete") + # everything complete + self.ioState = COMPLETED + self.trigger() + + def abort(self, err): + """Called by a client to abort all of the member transactions. + When the last pending member is aborted the group callback + function will be called.""" + if _debug: IOGroup._debug("abort %r", err) + + # change the state to reflect that it was killed + self.ioState = ABORTED + self.ioError = err + + # abort all the members + for iocb in self.ioMembers: + iocb.abort(err) + + # notify the client + self.trigger() + +# +# IOQueue +# + +@bacpypes_debugging +class IOQueue: + + def __init__(self, name=None): + if _debug: IOQueue._debug("__init__ %r", name) + + self.notempty = threading.Event() + self.notempty.clear() + + self.queue = [] + + def put(self, iocb): + """Add an IOCB to a queue. This is usually called by the function + that filters requests and passes them out to the correct processing + thread.""" + if _debug: IOQueue._debug("put %r", iocb) + + # requests should be pending before being queued + if iocb.ioState != PENDING: + raise RuntimeError("invalid state transition") + + # save that it might have been empty + wasempty = not self.notempty.isSet() + + # add the request to the end of the list of iocb's at same priority + priority = iocb.ioPriority + item = (priority, iocb) + self.queue.insert(bisect_left(self.queue, (priority+1,)), item) + + # point the iocb back to this queue + iocb.ioQueue = self + + # set the event, queue is no longer empty + self.notempty.set() + + return wasempty + + def get(self, block=1, delay=None): + """Get a request from a queue, optionally block until a request + is available.""" + if _debug: IOQueue._debug("get block=%r delay=%r", block, delay) + + # if the queue is empty and we do not block return None + if not block and not self.notempty.isSet(): + return None + + # wait for something to be in the queue + if delay: + self.notempty.wait(delay) + if not self.notempty.isSet(): + return None + else: + self.notempty.wait() + + # extract the first element + priority, iocb = self.queue[0] + del self.queue[0] + iocb.ioQueue = None + + # if the queue is empty, clear the event + qlen = len(self.queue) + if not qlen: + self.notempty.clear() + + # return the request + return iocb + + def remove(self, iocb): + """Remove a control block from the queue, called if the request + is canceled/aborted.""" + if _debug: IOQueue._debug("remove %r", iocb) + + # remove the request from the queue + for i, item in enumerate(self.queue): + if iocb is item[1]: + if _debug: IOQueue._debug(" - found at %d", i) + del self.queue[i] + + # if the queue is empty, clear the event + qlen = len(self.queue) + if not qlen: + self.notempty.clear() + + # record the new length + # self.queuesize.Record( qlen, _time() ) + break + else: + if _debug: IOQueue._debug(" - not found") + + def abort(self, err): + """Abort all of the control blocks in the queue.""" + if _debug: IOQueue._debug("abort %r", err) + + # send aborts to all of the members + try: + for iocb in self.queue: + iocb.ioQueue = None + iocb.abort(err) + + # flush the queue + self.queue = [] + + # the queue is now empty, clear the event + self.notempty.clear() + except ValueError: + pass + +# +# IOController +# + +@bacpypes_debugging +class IOController(object): + + def __init__(self, name=None): + """Initialize a controller.""" + if _debug: IOController._debug("__init__ name=%r", name) + + # save the name + self.name = name + + def abort(self, err): + """Abort all requests, no default implementation.""" + pass + + def request_io(self, iocb): + """Called by a client to start processing a request.""" + if _debug: IOController._debug("request_io %r", iocb) + + # check that the parameter is an IOCB + if not isinstance(iocb, IOCB): + raise TypeError("IOCB expected") + + # bind the iocb to this controller + iocb.ioController = self + + try: + # hopefully there won't be an error + err = None + + # change the state + iocb.ioState = PENDING + + # let derived class figure out how to process this + self.process_io(iocb) + except: + # extract the error + err = sys.exc_info()[1] + + # if there was an error, abort the request + if err: + self.abort_io(iocb, err) + + def process_io(self, iocb): + """Figure out how to respond to this request. This must be + provided by the derived class.""" + raise NotImplementedError("IOController must implement process_io()") + + def active_io(self, iocb): + """Called by a handler to notify the controller that a request is + being processed.""" + if _debug: IOController._debug("active_io %r", iocb) + + # requests should be idle or pending before coming active + if (iocb.ioState != IDLE) and (iocb.ioState != PENDING): + raise RuntimeError("invalid state transition (currently %d)" % (iocb.ioState,)) + + # change the state + iocb.ioState = ACTIVE + + def complete_io(self, iocb, msg): + """Called by a handler to return data to the client.""" + if _debug: IOController._debug("complete_io %r %r", iocb, msg) + + # if it completed, leave it alone + if iocb.ioState == COMPLETED: + pass + + # if it already aborted, leave it alone + elif iocb.ioState == ABORTED: + pass + + else: + # change the state + iocb.ioState = COMPLETED + iocb.ioResponse = msg + + # notify the client + iocb.trigger() + + def abort_io(self, iocb, err): + """Called by a handler or a client to abort a transaction.""" + if _debug: IOController._debug("abort_io %r %r", iocb, err) + + # if it completed, leave it alone + if iocb.ioState == COMPLETED: + pass + + # if it already aborted, leave it alone + elif iocb.ioState == ABORTED: + pass + + else: + # change the state + iocb.ioState = ABORTED + iocb.ioError = err + + # notify the client + iocb.trigger() + +# +# IOQController +# + +@bacpypes_debugging +class IOQController(IOController): + + wait_time = 0.0 + + def __init__(self, name=None): + """Initialize a queue controller.""" + if _debug: IOQController._debug("__init__ name=%r", name) + IOController.__init__(self, name) + + # start idle + self.state = CTRL_IDLE + _statelog.debug("%s %s %s" % (_strftime(), self.name, "idle")) + + # no active iocb + self.active_iocb = None + + # create an IOQueue for iocb's requested when not idle + self.ioQueue = IOQueue(str(name) + " queue") + + def abort(self, err): + """Abort all pending requests.""" + if _debug: IOQController._debug("abort %r", err) + + if (self.state == CTRL_IDLE): + if _debug: IOQController._debug(" - idle") + return + + while True: + iocb = self.ioQueue.get() + if not iocb: + break + if _debug: IOQController._debug(" - iocb: %r", iocb) + + # change the state + iocb.ioState = ABORTED + iocb.ioError = err + + # notify the client + iocb.trigger() + + if (self.state != CTRL_IDLE): + if _debug: IOQController._debug(" - busy after aborts") + + def request_io(self, iocb): + """Called by a client to start processing a request.""" + if _debug: IOQController._debug("request_io %r", iocb) + + # bind the iocb to this controller + iocb.ioController = self + + # if we're busy, queue it + if (self.state != CTRL_IDLE): + if _debug: IOQController._debug(" - busy, request queued") + + iocb.ioState = PENDING + self.ioQueue.put(iocb) + return + + try: + # hopefully there won't be an error + err = None + + # let derived class figure out how to process this + self.process_io(iocb) + except: + # extract the error + err = sys.exc_info()[1] + + # if there was an error, abort the request + if err: + self.abort_io(iocb, err) + + def process_io(self, iocb): + """Figure out how to respond to this request. This must be + provided by the derived class.""" + raise NotImplementedError("IOController must implement process_io()") + + def active_io(self, iocb): + """Called by a handler to notify the controller that a request is + being processed.""" + if _debug: IOQController._debug("active_io %r", iocb) + + # base class work first, setting iocb state and timer data + IOController.active_io(self, iocb) + + # change our state + self.state = CTRL_ACTIVE + _statelog.debug("%s %s %s" % (_strftime(), self.name, "active")) + + # keep track of the iocb + self.active_iocb = iocb + + def complete_io(self, iocb, msg): + """Called by a handler to return data to the client.""" + if _debug: IOQController._debug("complete_io %r %r", iocb, msg) + + # check to see if it is completing the active one + if iocb is not self.active_iocb: + raise RuntimeError("not the current iocb") + + # normal completion + IOController.complete_io(self, iocb, msg) + + # no longer an active iocb + self.active_iocb = None + + # check to see if we should wait a bit + if self.wait_time: + # change our state + self.state = CTRL_WAITING + _statelog.debug("%s %s %s" % (_strftime(), self.name, "waiting")) + + # schedule a call in the future + task = FunctionTask(IOQController._wait_trigger, self) + task.install_task(_time() + self.wait_time) + + else: + # change our state + self.state = CTRL_IDLE + _statelog.debug("%s %s %s" % (_strftime(), self.name, "idle")) + + # look for more to do + deferred(IOQController._trigger, self) + + def abort_io(self, iocb, err): + """Called by a handler or a client to abort a transaction.""" + if _debug: IOQController._debug("abort_io %r %r", iocb, err) + + # normal abort + IOController.abort_io(self, iocb, err) + + # check to see if it is completing the active one + if iocb is not self.active_iocb: + if _debug: IOQController._debug(" - not current iocb") + return + + # no longer an active iocb + self.active_iocb = None + + # change our state + self.state = CTRL_IDLE + _statelog.debug("%s %s %s" % (_strftime(), self.name, "idle")) + + # look for more to do + deferred(IOQController._trigger, self) + + def _trigger(self): + """Called to launch the next request in the queue.""" + if _debug: IOQController._debug("_trigger") + + # if we are busy, do nothing + if self.state != CTRL_IDLE: + if _debug: IOQController._debug(" - not idle") + return + + # if there is nothing to do, return + if not self.ioQueue.queue: + if _debug: IOQController._debug(" - empty queue") + return + + # get the next iocb + iocb = self.ioQueue.get() + + try: + # hopefully there won't be an error + err = None + + # let derived class figure out how to process this + self.process_io(iocb) + except: + # extract the error + err = sys.exc_info()[1] + + # if there was an error, abort the request + if err: + self.abort_io(iocb, err) + + # if we're idle, call again + if self.state == CTRL_IDLE: + deferred(IOQController._trigger, self) + + def _wait_trigger(self): + """Called to launch the next request in the queue.""" + if _debug: IOQController._debug("_wait_trigger") + + # make sure we are waiting + if (self.state != CTRL_WAITING): + raise RuntimeError("not waiting") + + # change our state + self.state = CTRL_IDLE + _statelog.debug("%s %s %s" % (_strftime(), self.name, "idle")) + + # look for more to do + IOQController._trigger(self) + +# +# ClientController +# + +@bacpypes_debugging +class ClientController(Client, IOQController): + + def __init__(self): + if _debug: ClientController._debug("__init__") + Client.__init__(self) + IOQController.__init__(self) + + def process_io(self, iocb): + if _debug: ClientController._debug("process_io %r", iocb) + + # this is now an active request + self.active_io(iocb) + + # send the PDU downstream + self.request(iocb.args[0]) + + def confirmation(self, pdu): + if _debug: ClientController._debug("confirmation %r %r", args, kwargs) + + # make sure it has an active iocb + if not self.active_iocb: + ClientController._debug("no active request") + return + + # look for exceptions + if isinstance(pdu, Exception): + self.abort_io(self.active_iocb, pdu) + else: + self.complete_io(self.active_iocb, pdu) + +# +# SieveQueue +# + +@bacpypes_debugging +class SieveQueue(IOQController): + + def __init__(self, request_fn, address=None): + if _debug: SieveQueue._debug("__init__ %r %r", request_fn, address) + IOQController.__init__(self, str(address)) + + # save a reference to the request function + self.request_fn = request_fn + self.address = address + + def process_io(self, iocb): + if _debug: SieveQueue._debug("process_io %r", iocb) + + # this is now an active request + self.active_io(iocb) + + # send the request + self.request_fn(iocb.args[0]) + +# +# SieveClientController +# + +@bacpypes_debugging +class SieveClientController(Client, IOController): + + def __init__(self): + if _debug: SieveClientController._debug("__init__") + Client.__init__(self) + IOController.__init__(self) + + # queues for each address + self.queues = {} + + def process_io(self, iocb): + if _debug: SieveClientController._debug("process_io %r", iocb) + + # get the destination address from the pdu + destination_address = iocb.args[0].pduDestination + if _debug: SieveClientController._debug(" - destination_address: %r", destination_address) + + # look up the queue + queue = self.queues.get(destination_address, None) + if not queue: + queue = SieveQueue(self.request, destination_address) + self.queues[destination_address] = queue + if _debug: SieveClientController._debug(" - queue: %r", queue) + + # ask the queue to process the request + queue.request_io(iocb) + + def request(self, pdu): + if _debug: SieveClientController._debug("request %r", pdu) + + # send it downstream + super(SieveClientController, self).request(pdu) + + def confirmation(self, pdu): + if _debug: SieveClientController._debug("confirmation %r", pdu) + + # get the source address + source_address = pdu.pduSource + if _debug: SieveClientController._debug(" - source_address: %r", source_address) + + # look up the queue + queue = self.queues.get(source_address, None) + if not queue: + SieveClientController._debug("no queue for %r" % (source_address,)) + return + if _debug: SieveClientController._debug(" - queue: %r", queue) + + # make sure it has an active iocb + if not queue.active_iocb: + SieveClientController._debug("no active request for %r" % (source_address,)) + return + + # complete the request + if isinstance(pdu, Exception): + queue.abort_io(queue.active_iocb, pdu) + else: + queue.complete_io(queue.active_iocb, pdu) + + # if the queue is empty and idle, forget about the controller + if not queue.ioQueue.queue and not queue.active_iocb: + if _debug: SieveClientController._debug(" - queue is empty") + del self.queues[source_address] + +# +# register_controller +# + +@bacpypes_debugging +def register_controller(controller): + if _debug: register_controller._debug("register_controller %r", controller) + global local_controllers + + # skip those that shall not be named + if not controller.name: + return + + # make sure there isn't one already + if controller.name in local_controllers: + raise RuntimeError("already a local controller named %r" % (controller.name,)) + + local_controllers[controller.name] = controller + +# +# abort +# + +@bacpypes_debugging +def abort(err): + """Abort everything, everywhere.""" + if _debug: abort._debug("abort %r", err) + global local_controllers + + # tell all the local controllers to abort + for controller in local_controllers.values(): + controller.abort(err) diff --git a/py27/bacpypes/object.py b/py27/bacpypes/object.py index 6bc95cc4..28ff80fc 100755 --- a/py27/bacpypes/object.py +++ b/py27/bacpypes/object.py @@ -5,6 +5,8 @@ """ import sys +from copy import copy as _copy +from collections import defaultdict from .errors import ConfigurationError, ExecutionError, \ InvalidParameterDatatype @@ -173,8 +175,11 @@ def ReadProperty(self, obj, arrayIndex=None): raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') if value is not None: - # dive in, the water's fine - value = value[arrayIndex] + try: + # dive in, the water's fine + value = value[arrayIndex] + except IndexError: + raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') # all set return value @@ -210,6 +215,9 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False self.identifier, self.datatype.__name__, )) + # local check if the property is monitored + is_monitored = self.identifier in obj._property_monitors + if arrayIndex is not None: if not issubclass(self.datatype, Array): raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') @@ -219,14 +227,34 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False if arry is None: raise RuntimeError("%s uninitialized array" % (self.identifier,)) + if is_monitored: + old_value = _copy(arry) + # seems to be OK, let the array object take over if _debug: Property._debug(" - forwarding to array") - arry[arrayIndex] = value + try: + arry[arrayIndex] = value + except IndexError: + raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') - return + # check for monitors, call each one with the old and new value + if is_monitored: + for fn in obj._property_monitors[self.identifier]: + if _debug: Property._debug(" - monitor: %r", fn) + fn(old_value, arry) + + else: + if is_monitored: + old_value = obj._values.get(self.identifier, None) - # seems to be OK - obj._values[self.identifier] = value + # seems to be OK + obj._values[self.identifier] = value + + # check for monitors, call each one with the old and new value + if is_monitored: + for fn in obj._property_monitors[self.identifier]: + if _debug: Property._debug(" - monitor: %r", fn) + fn(old_value, value) # # StandardProperty @@ -365,6 +393,9 @@ def __init__(self, **kwargs): # start with a clean dict of values self._values = {} + # empty list of property monitors + self._property_monitors = defaultdict(list) + # start with a clean array of property identifiers if 'propertyList' in initargs: propertyList = None @@ -440,6 +471,49 @@ def __setattr__(self, attr, value): return prop.WriteProperty(self, value, direct=True) + def add_property(self, prop): + """Add a property to an object. The property is an instance of + a Property or one of its derived classes. Adding a property + disconnects it from the collection of properties common to all of the + objects of its class.""" + if _debug: Object._debug("add_property %r", prop) + + # make a copy of the properties dictionary + self._properties = _copy(self._properties) + + # save the property reference and default value (usually None) + self._properties[prop.identifier] = prop + self._values[prop.identifier] = prop.default + + # tell the object it has a new property + if 'propertyList' in self._values: + property_list = self.propertyList + if prop.identifier not in property_list: + if _debug: Object._debug(" - adding to property list") + property_list.append(prop.identifier) + + def delete_property(self, prop): + """Delete a property from an object. The property is an instance of + a Property or one of its derived classes, but only the property + is relavent. Deleting a property disconnects it from the collection of + properties common to all of the objects of its class.""" + if _debug: Object._debug("delete_property %r", value) + + # make a copy of the properties dictionary + self._properties = _copy(self._properties) + + # delete the property from the dictionary and values + del self._properties[prop.identifier] + if prop.identifier in self._values: + del self._values[prop.identifier] + + # remove the property identifier from its list of know properties + if 'propertyList' in self._values: + property_list = self.propertyList + if prop.identifier in property_list: + if _debug: Object._debug(" - removing from property list") + property_list.remove(prop.identifier) + def ReadProperty(self, propid, arrayIndex=None): if _debug: Object._debug("ReadProperty %r arrayIndex=%r", propid, arrayIndex) @@ -529,13 +603,14 @@ def debug_contents(self, indent=1, file=sys.stdout, _ids=None): file.write("%s%s = %s\n" % (" " * indent, attr, getattr(self, attr))) previous_attrs = attrs - # build a list of properties "bottom up" + # build a list of property identifiers "bottom up" property_names = [] + properties_seen = set() for c in klasses: - properties = getattr(c, 'properties', []) - for property in properties: - if property.identifier not in property_names: - property_names.append(property.identifier) + for prop in getattr(c, 'properties', []): + if prop.identifier not in properties_seen: + property_names.append(prop.identifier) + properties_seen.add(prop.identifier) # print out the values for property_name in property_names: @@ -581,9 +656,9 @@ class AccessCredentialObject(Object): , OptionalProperty('extendedTimeEnable', Boolean) , OptionalProperty('authorizationExemptions', SequenceOf(AuthorizationException)) , OptionalProperty('reliabilityEvaluationInhibit', Boolean) - , OptionalProperty('masterExemption', Boolean) - , OptionalProperty('passbackExemption', Boolean) - , OptionalProperty('occupancyExemption', Boolean) +# , OptionalProperty('masterExemption', Boolean) +# , OptionalProperty('passbackExemption', Boolean) +# , OptionalProperty('occupancyExemption', Boolean) ] @register_object_type @@ -1265,6 +1340,7 @@ class DeviceObject(Object): , OptionalProperty('timeSynchronizationInterval', Unsigned) , OptionalProperty('alignIntervals', Boolean) , OptionalProperty('intervalOffset', Unsigned) + , OptionalProperty('serialNumber', CharacterString) ] @register_object_type diff --git a/py27/bacpypes/primitivedata.py b/py27/bacpypes/primitivedata.py index b110eb44..474bd393 100755 --- a/py27/bacpypes/primitivedata.py +++ b/py27/bacpypes/primitivedata.py @@ -1245,7 +1245,7 @@ class Date(Atomic): def __init__(self, arg=None, year=255, month=255, day=255, day_of_week=255): self.value = (year, month, day, day_of_week) - + if arg is None: pass elif isinstance(arg, Tag): @@ -1363,7 +1363,7 @@ def CalcDayOfWeek(self): elif day in _special_day_inv: pass else: - try: + try: today = time.mktime( (year + 1900, month, day, 0, 0, 0, 0, 0, -1) ) day_of_week = time.gmtime(today)[6] + 1 except OverflowError: diff --git a/py27/bacpypes/service/__init__.py b/py27/bacpypes/service/__init__.py new file mode 100644 index 00000000..69329988 --- /dev/null +++ b/py27/bacpypes/service/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python + +""" +Service Subpackage +""" + +from . import test +from . import detect + +from . import device +from . import object +from . import cov +from . import file diff --git a/py27/bacpypes/service/cov.py b/py27/bacpypes/service/cov.py new file mode 100644 index 00000000..d8630d71 --- /dev/null +++ b/py27/bacpypes/service/cov.py @@ -0,0 +1,641 @@ +#!/usr/bin/env python + +""" +Change Of Value Service +""" + +from ..debugging import bacpypes_debugging, DebugContents, ModuleLogger +from ..capability import Capability + +from ..task import OneShotTask, TaskManager +from ..iocb import IOCB + +from ..basetypes import DeviceAddress, COVSubscription, PropertyValue, \ + Recipient, RecipientProcess, ObjectPropertyReference +from ..constructeddata import SequenceOf, Any +from ..apdu import ConfirmedCOVNotificationRequest, \ + UnconfirmedCOVNotificationRequest, \ + SimpleAckPDU, Error, RejectPDU, AbortPDU +from ..errors import ExecutionError + +from ..object import Property +from .detect import DetectionAlgorithm, monitor_filter + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# SubscriptionList +# + +@bacpypes_debugging +class SubscriptionList: + + def __init__(self): + if _debug: SubscriptionList._debug("__init__") + + self.cov_subscriptions = [] + + def append(self, cov): + if _debug: SubscriptionList._debug("append %r", cov) + + self.cov_subscriptions.append(cov) + + def remove(self, cov): + if _debug: SubscriptionList._debug("remove %r", cov) + + self.cov_subscriptions.remove(cov) + + def find(self, client_addr, proc_id, obj_id): + if _debug: SubscriptionList._debug("find %r %r %r", client_addr, proc_id, obj_id) + + for cov in self.cov_subscriptions: + all_equal = (cov.client_addr == client_addr) and \ + (cov.proc_id == proc_id) and \ + (cov.obj_id == obj_id) + if _debug: SubscriptionList._debug(" - cov, all_equal: %r %r", cov, all_equal) + + if all_equal: + return cov + + return None + + def __len__(self): + if _debug: SubscriptionList._debug("__len__") + + return len(self.cov_subscriptions) + + def __iter__(self): + if _debug: SubscriptionList._debug("__iter__") + + for cov in self.cov_subscriptions: + yield cov + + +# +# Subscription +# + +@bacpypes_debugging +class Subscription(OneShotTask, DebugContents): + + _debug_contents = ( + 'obj_ref', + 'client_addr', + 'proc_id', + 'obj_id', + 'confirmed', + 'lifetime', + ) + + def __init__(self, obj_ref, client_addr, proc_id, obj_id, confirmed, lifetime): + if _debug: Subscription._debug("__init__ %r %r %r %r %r %r", obj_ref, client_addr, proc_id, obj_id, confirmed, lifetime) + OneShotTask.__init__(self) + + # save the reference to the related object + self.obj_ref = obj_ref + + # save the parameters + self.client_addr = client_addr + self.proc_id = proc_id + self.obj_id = obj_id + self.confirmed = confirmed + self.lifetime = lifetime + + # if lifetime is non-zero, schedule the subscription to expire + if lifetime != 0: + self.install_task(delta=self.lifetime) + + def cancel_subscription(self): + if _debug: Subscription._debug("cancel_subscription") + + # suspend the task + self.suspend_task() + + # tell the application to cancel us + self.obj_ref._app.cancel_subscription(self) + + # break the object reference + self.obj_ref = None + + def renew_subscription(self, lifetime): + if _debug: Subscription._debug("renew_subscription") + + # suspend iff scheduled + if self.isScheduled: + self.suspend_task() + + # reschedule the task if its not infinite + if lifetime != 0: + self.install_task(delta=lifetime) + + def process_task(self): + if _debug: Subscription._debug("process_task") + + # subscription is canceled + self.cancel_subscription() + +# +# COVDetection +# + +@bacpypes_debugging +class COVDetection(DetectionAlgorithm): + + properties_tracked = () + properties_reported = () + monitored_property_reference = None + + def __init__(self, obj): + if _debug: COVDetection._debug("__init__ %r", obj) + DetectionAlgorithm.__init__(self) + + # keep track of the object + self.obj = obj + + # build a list of parameters and matching object property references + kwargs = {} + for property_name in self.properties_tracked: + setattr(self, property_name, None) + kwargs[property_name] = (obj, property_name) + + # let the base class set up the bindings and initial values + self.bind(**kwargs) + + # list of all active subscriptions + self.cov_subscriptions = SubscriptionList() + + def execute(self): + if _debug: COVDetection._debug("execute") + + # something changed, send out the notifications + self.send_cov_notifications() + + def send_cov_notifications(self): + if _debug: COVDetection._debug("send_cov_notifications") + + # check for subscriptions + if not len(self.cov_subscriptions): + return + + # get the current time from the task manager + current_time = TaskManager().get_time() + if _debug: COVDetection._debug(" - current_time: %r", current_time) + + # create a list of values + list_of_values = [] + for property_name in self.properties_reported: + if _debug: COVDetection._debug(" - property_name: %r", property_name) + + # get the class + property_datatype = self.obj.get_datatype(property_name) + if _debug: COVDetection._debug(" - property_datatype: %r", property_datatype) + + # build the value + bundle_value = property_datatype(self.obj._values[property_name]) + if _debug: COVDetection._debug(" - bundle_value: %r", bundle_value) + + # bundle it into a sequence + property_value = PropertyValue( + propertyIdentifier=property_name, + value=Any(bundle_value), + ) + + # add it to the list + list_of_values.append(property_value) + if _debug: COVDetection._debug(" - list_of_values: %r", list_of_values) + + # loop through the subscriptions and send out notifications + for cov in self.cov_subscriptions: + if _debug: COVDetection._debug(" - cov: %s", repr(cov)) + + # calculate time remaining + if not cov.lifetime: + time_remaining = 0 + else: + time_remaining = int(cov.taskTime - current_time) + + # make sure it is at least one second + if not time_remaining: + time_remaining = 1 + + # build a request with the correct type + if cov.confirmed: + request = ConfirmedCOVNotificationRequest() + else: + request = UnconfirmedCOVNotificationRequest() + + # fill in the parameters + request.pduDestination = cov.client_addr + request.subscriberProcessIdentifier = cov.proc_id + request.initiatingDeviceIdentifier = self.obj._app.localDevice.objectIdentifier + request.monitoredObjectIdentifier = cov.obj_id + request.timeRemaining = time_remaining + request.listOfValues = list_of_values + if _debug: COVDetection._debug(" - request: %s", repr(request)) + + # let the application send it + self.obj._app.cov_notification(cov, request) + + def __str__(self): + return "<" + self.__class__.__name__ + \ + "(" + ','.join(self.properties_tracked) + ')' + \ + ">" + +class GenericCriteria(COVDetection): + + properties_tracked = ( + 'presentValue', + 'statusFlags', + ) + properties_reported = ( + 'presentValue', + 'statusFlags', + ) + monitored_property_reference = 'presentValue' + + +@bacpypes_debugging +class COVIncrementCriteria(COVDetection): + + properties_tracked = ( + 'presentValue', + 'statusFlags', + 'covIncrement', + ) + properties_reported = ( + 'presentValue', + 'statusFlags', + ) + monitored_property_reference = 'presentValue' + + def __init__(self, obj): + if _debug: COVIncrementCriteria._debug("__init__ %r", obj) + COVDetection.__init__(self, obj) + + # previous reported value + self.previous_reported_value = None + + @monitor_filter('presentValue') + def present_value_filter(self, old_value, new_value): + if _debug: COVIncrementCriteria._debug("present_value_filter %r %r", old_value, new_value) + + # first time around initialize to the old value + if self.previous_reported_value is None: + if _debug: COVIncrementCriteria._debug(" - first value: %r", old_value) + self.previous_reported_value = old_value + + # see if it changed enough to trigger reporting + value_changed = (new_value <= (self.previous_reported_value - self.covIncrement)) \ + or (new_value >= (self.previous_reported_value + self.covIncrement)) + if _debug: COVIncrementCriteria._debug(" - value significantly changed: %r", value_changed) + + return value_changed + + def send_cov_notifications(self): + if _debug: COVIncrementCriteria._debug("send_cov_notifications") + + # when sending out notifications, keep the current value + self.previous_reported_value = self.presentValue + + # continue + COVDetection.send_cov_notifications(self) + + +class AccessDoorCriteria(COVDetection): + + properties_tracked = ( + 'presentValue', + 'statusFlags', + 'doorAlarmState', + ) + properties_reported = ( + 'presentValue', + 'statusFlags', + 'doorAlarmState', + ) + +class AccessPointCriteria(COVDetection): + + properties_tracked = ( + 'accessEventTime', + 'statusFlags', + ) + properties_reported = ( + 'accessEvent', + 'statusFlags', + 'accessEventTag', + 'accessEventTime', + 'accessEventCredential', + 'accessEventAuthenticationFactor', + ) + monitored_property_reference = 'accessEvent' + +class CredentialDataInputCriteria(COVDetection): + + properties_tracked = ( + 'updateTime', + 'statusFlags' + ) + properties_reported = ( + 'presentValue', + 'statusFlags', + 'updateTime', + ) + +class LoadControlCriteria(COVDetection): + + properties_tracked = ( + 'presentValue', + 'statusFlags', + 'requestedShedLevel', + 'startTime', + 'shedDuration', + 'dutyWindow', + ) + properties_reported = ( + 'presentValue', + 'statusFlags', + 'requestedShedLevel', + 'startTime', + 'shedDuration', + 'dutyWindow', + ) + +class PulseConverterCriteria(COVDetection): + + properties_tracked = ( + 'presentValue', + 'statusFlags', + ) + properties_reported = ( + 'presentValue', + 'statusFlags', + ) + +# mapping from object type to appropriate criteria class +criteria_type_map = { + 'accessPoint': AccessPointCriteria, + 'analogInput': COVIncrementCriteria, + 'analogOutput': COVIncrementCriteria, + 'analogValue': COVIncrementCriteria, + 'largeAnalogValue': COVIncrementCriteria, + 'integerValue': COVIncrementCriteria, + 'positiveIntegerValue': COVIncrementCriteria, + 'lightingOutput': COVIncrementCriteria, + 'binaryInput': GenericCriteria, + 'binaryOutput': GenericCriteria, + 'binaryValue': GenericCriteria, + 'lifeSafetyPoint': GenericCriteria, + 'lifeSafetyZone': GenericCriteria, + 'multiStateInput': GenericCriteria, + 'multiStateOutput': GenericCriteria, + 'multiStateValue': GenericCriteria, + 'octetString': GenericCriteria, + 'characterString': GenericCriteria, + 'timeValue': GenericCriteria, + 'dateTimeValue': GenericCriteria, + 'dateValue': GenericCriteria, + 'timePatternValue': GenericCriteria, + 'datePatternValue': GenericCriteria, + 'dateTimePatternValue': GenericCriteria, + 'credentialDataInput': CredentialDataInputCriteria, + 'loadControl': LoadControlCriteria, + 'pulseConverter': PulseConverterCriteria, + } + +# +# ActiveCOVSubscriptions +# + +@bacpypes_debugging +class ActiveCOVSubscriptions(Property): + + def __init__(self): + Property.__init__( + self, 'activeCovSubscriptions', SequenceOf(COVSubscription), + default=None, optional=True, mutable=False, + ) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: ActiveCOVSubscriptions._debug("ReadProperty %s arrayIndex=%r", obj, arrayIndex) + + # get the current time from the task manager + current_time = TaskManager().get_time() + if _debug: ActiveCOVSubscriptions._debug(" - current_time: %r", current_time) + + # start with an empty sequence + cov_subscriptions = SequenceOf(COVSubscription)() + + # loop through the object and detection list + for obj, cov_detection in self.cov_detections.items(): + for cov in cov_detection.cov_subscriptions: + # calculate time remaining + if not cov.lifetime: + time_remaining = 0 + else: + time_remaining = int(cov.taskTime - current_time) + + # make sure it is at least one second + if not time_remaining: + time_remaining = 1 + + recipient_process = RecipientProcess( + recipient=Recipient( + address=DeviceAddress( + networkNumber=cov.client_addr.addrNet or 0, + macAddress=cov.client_addr.addrAddr, + ), + ), + processIdentifier=cov.proc_id, + ) + + cov_subscription = COVSubscription( + recipient=recipient_process, + monitoredPropertyReference=ObjectPropertyReference( + objectIdentifier=cov.obj_id, + propertyIdentifier=cov_detection.monitored_property_reference, + ), + issueConfirmedNotifications=cov.confirmed, + timeRemaining=time_remaining, + ) + if hasattr(cov_detection, 'covIncrement'): + cov_subscription.covIncrement = cov_detection.covIncrement + if _debug: ActiveCOVSubscriptions._debug(" - cov_subscription: %r", cov_subscription) + + # add the list + cov_subscriptions.append(cov_subscription) + + # add the list + cov_subscriptions.append(cov_subscription) + + return cov_subscriptions + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + + +# +# ChangeOfValueServices +# + +@bacpypes_debugging +class ChangeOfValueServices(Capability): + + def __init__(self): + if _debug: ChangeOfValueServices._debug("__init__") + Capability.__init__(self) + + # map from an object to its detection algorithm + self.cov_detections = {} + + # if there is a local device object, make sure it has an active COV + # subscriptions property + if self.localDevice and self.localDevice.activeCovSubscriptions is None: + self.localDevice.add_property(ActiveCOVSubscriptions()) + + def add_subscription(self, cov): + if _debug: ChangeOfValueServices._debug("add_subscription %r", cov) + + # add it to the subscription list for its object + self.cov_detections[cov.obj_ref].cov_subscriptions.append(cov) + + def cancel_subscription(self, cov): + if _debug: ChangeOfValueServices._debug("cancel_subscription %r", cov) + + # cancel the subscription timeout + if cov.isScheduled: + cov.suspend_task() + if _debug: ChangeOfValueServices._debug(" - task suspended") + + # get the detection algorithm object + cov_detection = self.cov_detections[cov.obj_ref] + + # remove it from the subscription list for its object + cov_detection.cov_subscriptions.remove(cov) + + # if the detection algorithm doesn't have any subscriptions, remove it + if not len(cov_detection.cov_subscriptions): + if _debug: ChangeOfValueServices._debug(" - no more subscriptions") + + # unbind all the hooks into the object + cov_detection.unbind() + + # delete it from the object map + del self.cov_detections[cov.obj_ref] + + def cov_notification(self, cov, request): + if _debug: ChangeOfValueServices._debug("cov_notification %s %s", str(cov), str(request)) + + # create an IOCB with the request + iocb = IOCB(request) + if _debug: ChangeOfValueServices._debug(" - iocb: %r", iocb) + + # add a callback for the response, even if it was unconfirmed + iocb.cov = cov + iocb.add_callback(self.cov_confirmation) + + # send the request via the ApplicationIOController + self.request_io(iocb) + + def cov_confirmation(self, iocb): + if _debug: ChangeOfValueServices._debug("cov_confirmation %r", iocb) + + # do something for success + if iocb.ioResponse: + if _debug: ChangeOfValueServices._debug(" - ack") + self.cov_ack(iocb.cov, iocb.args[0], iocb.ioResponse) + + elif isinstance(iocb.ioError, Error): + if _debug: ChangeOfValueServices._debug(" - error: %r", iocb.ioError.errorCode) + self.cov_error(iocb.cov, iocb.args[0], iocb.ioError) + + elif isinstance(iocb.ioError, RejectPDU): + if _debug: ChangeOfValueServices._debug(" - reject: %r", iocb.ioError.apduAbortRejectReason) + self.cov_reject(iocb.cov, iocb.args[0], iocb.ioError) + + elif isinstance(iocb.ioError, AbortPDU): + if _debug: ChangeOfValueServices._debug(" - abort: %r", iocb.ioError.apduAbortRejectReason) + self.cov_abort(iocb.cov, iocb.args[0], iocb.ioError) + + def cov_ack(self, cov, request, response): + if _debug: ChangeOfValueServices._debug("cov_ack %r %r %r", cov, request, response) + + def cov_error(self, cov, request, response): + if _debug: ChangeOfValueServices._debug("cov_error %r %r %r", cov, request, response) + + def cov_reject(self, cov, request, response): + if _debug: ChangeOfValueServices._debug("cov_reject %r %r %r", cov, request, response) + + def cov_abort(self, cov, request, response): + if _debug: ChangeOfValueServices._debug("cov_abort %r %r %r", cov, request, response) + + ### delete the rest of the pending requests for this client + + def do_SubscribeCOVRequest(self, apdu): + if _debug: ChangeOfValueServices._debug("do_SubscribeCOVRequest %r", apdu) + + # extract the pieces + client_addr = apdu.pduSource + proc_id = apdu.subscriberProcessIdentifier + obj_id = apdu.monitoredObjectIdentifier + confirmed = apdu.issueConfirmedNotifications + lifetime = apdu.lifetime + + # request is to cancel the subscription + cancel_subscription = (confirmed is None) and (lifetime is None) + + # find the object + obj = self.get_object_id(obj_id) + if _debug: ChangeOfValueServices._debug(" - object: %r", obj) + if not obj: + raise ExecutionError(errorClass='object', errorCode='unknownObject') + + # look for an algorithm already associated with this object + cov_detection = self.cov_detections.get(obj, None) + + # if there isn't one, make one and associate it with the object + if not cov_detection: + # look for an associated class and if it's not there it's not supported + criteria_class = criteria_type_map.get(obj_id[0], None) + if not criteria_class: + raise ExecutionError(errorClass='services', errorCode='covSubscriptionFailed') + + # make one of these and bind it to the object + cov_detection = criteria_class(obj) + + # keep track of it for other subscriptions + self.cov_detections[obj] = cov_detection + if _debug: ChangeOfValueServices._debug(" - cov_detection: %r", cov_detection) + + # can a match be found? + cov = cov_detection.cov_subscriptions.find(client_addr, proc_id, obj_id) + if _debug: ChangeOfValueServices._debug(" - cov: %r", cov) + + # if a match was found, update the subscription + if cov: + if cancel_subscription: + if _debug: ChangeOfValueServices._debug(" - cancel the subscription") + self.cancel_subscription(cov) + else: + if _debug: ChangeOfValueServices._debug(" - renew the subscription") + cov.renew_subscription(lifetime) + else: + if cancel_subscription: + if _debug: ChangeOfValueServices._debug(" - cancel a subscription that doesn't exist") + else: + if _debug: ChangeOfValueServices._debug(" - create a subscription") + + # make a subscription + cov = Subscription(obj, client_addr, proc_id, obj_id, confirmed, lifetime) + if _debug: ChangeOfValueServices._debug(" - cov: %r", cov) + + # add it to our subscriptions lists + self.add_subscription(cov) + + # success + response = SimpleAckPDU(context=apdu) + + # return the result + self.response(response) diff --git a/py27/bacpypes/service/detect.py b/py27/bacpypes/service/detect.py new file mode 100755 index 00000000..0be05513 --- /dev/null +++ b/py27/bacpypes/service/detect.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python + +""" +Detection +""" + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger + +from bacpypes.core import deferred + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# DetectionMonitor +# + +@bacpypes_debugging +class DetectionMonitor: + + def __init__(self, algorithm, parameter, obj, prop, filter=None): + if _debug: DetectionMonitor._debug("__init__ ...") + + # keep track of the parameter values + self.algorithm = algorithm + self.parameter = parameter + self.obj = obj + self.prop = prop + self.filter = None + + def property_change(self, old_value, new_value): + if _debug: DetectionMonitor._debug("property_change %r %r", old_value, new_value) + + # set the parameter value + setattr(self.algorithm, self.parameter, new_value) + + # if the algorithm is already triggered, don't bother checking for more + if self.algorithm._triggered: + if _debug: DetectionMonitor._debug(" - already triggered") + return + + # if there is a special filter, use it, otherwise use != + if self.filter: + trigger = self.filter(old_value, new_value) + else: + trigger = (old_value != new_value) + if _debug: DetectionMonitor._debug(" - trigger: %r", trigger) + + # trigger it + if trigger: + deferred(self.algorithm._execute) + if _debug: DetectionMonitor._debug(" - deferred: %r", self.algorithm._execute) + + self.algorithm._triggered = True + +# +# monitor_filter +# + +def monitor_filter(parameter): + def transfer_filter_decorator(fn): + fn._monitor_filter = parameter + return fn + + return transfer_filter_decorator + +# +# DetectionAlgorithm +# + +@bacpypes_debugging +class DetectionAlgorithm: + + def __init__(self): + if _debug: DetectionAlgorithm._debug("__init__") + + # monitor objects + self._monitors = [] + + # triggered flag, set when a parameter changed and the monitor + # schedules the algorithm to execute + self._triggered = False + + def bind(self, **kwargs): + if _debug: DetectionAlgorithm._debug("bind %r", kwargs) + + # build a map of methods that are filters. These have been decorated + # with monitor_filter, but they are unbound methods (or simply + # functions in Python3) at the time they are decorated but by looking + # for them now they are bound to this instance. + monitor_filters = {} + for attr_name in dir(self): + attr = getattr(self, attr_name) + if hasattr(attr, "_monitor_filter"): + monitor_filters[attr._monitor_filter] = attr + if _debug: DetectionAlgorithm._debug(" - monitor_filters: %r", monitor_filters) + + for parameter, (obj, prop) in kwargs.items(): + if not hasattr(self, parameter): + if _debug: DetectionAlgorithm._debug(" - no matching parameter: %r", parameter) + + # make a detection monitor + monitor = DetectionMonitor(self, parameter, obj, prop) + if _debug: DetectionAlgorithm._debug(" - monitor: %r", monitor) + + # check to see if there is a custom filter for it + if parameter in monitor_filters: + monitor.filter = monitor_filters[parameter] + + # keep track of all of these objects for if/when we unbind + self._monitors.append(monitor) + + # add the property value monitor function + obj._property_monitors[prop].append(monitor.property_change) + + # set the parameter value to the property value if it's not None + property_value = obj._values[prop] + if property_value is not None: + if _debug: DetectionAlgorithm._debug(" - %s: %r", parameter, property_value) + setattr(self, parameter, property_value) + + def unbind(self): + if _debug: DetectionAlgorithm._debug("unbind") + + # remove the property value monitor functions + for monitor in self._monitors: + if _debug: DetectionAlgorithm._debug(" - monitor: %r", monitor) + monitor.obj._property_monitors[monitor.prop].remove(monitor.property_change) + + # abandon the array + self._monitors = [] + + def _execute(self): + if _debug: DetectionAlgorithm._debug("_execute") + + # provided by the derived class + self.execute() + + # turn the trigger off + self._triggered = False + + def execute(self): + raise NotImplementedError("execute not implemented") diff --git a/py27/bacpypes/service/device.py b/py27/bacpypes/service/device.py new file mode 100644 index 00000000..cc257ae0 --- /dev/null +++ b/py27/bacpypes/service/device.py @@ -0,0 +1,339 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger +from ..capability import Capability + +from ..pdu import GlobalBroadcast +from ..primitivedata import Date, Time, ObjectIdentifier +from ..constructeddata import ArrayOf + +from ..apdu import WhoIsRequest, IAmRequest, IHaveRequest +from ..errors import ExecutionError, InconsistentParameters, \ + MissingRequiredParameter, ParameterOutOfRange +from ..object import register_object_type, registered_object_types, \ + Property, DeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# CurrentDateProperty +# + +class CurrentDateProperty(Property): + + def __init__(self, identifier): + Property.__init__(self, identifier, Date, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + # access an array + if arrayIndex is not None: + raise TypeError("{0} is unsubscriptable".format(self.identifier)) + + # get the value + now = Date() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentTimeProperty +# + +class CurrentTimeProperty(Property): + + def __init__(self, identifier): + Property.__init__(self, identifier, Time, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + # access an array + if arrayIndex is not None: + raise TypeError("{0} is unsubscriptable".format(self.identifier)) + + # get the value + now = Time() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# LocalDeviceObject +# + +@bacpypes_debugging +class LocalDeviceObject(DeviceObject): + + properties = \ + [ CurrentTimeProperty('localTime') + , CurrentDateProperty('localDate') + ] + + defaultProperties = \ + { 'maxApduLengthAccepted': 1024 + , 'segmentationSupported': 'segmentedBoth' + , 'maxSegmentsAccepted': 16 + , 'apduSegmentTimeout': 5000 + , 'apduTimeout': 3000 + , 'numberOfApduRetries': 3 + } + + def __init__(self, **kwargs): + if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) + + # fill in default property values not in kwargs + for attr, value in LocalDeviceObject.defaultProperties.items(): + if attr not in kwargs: + kwargs[attr] = value + + # check for registration + if self.__class__ not in registered_object_types.values(): + if 'vendorIdentifier' not in kwargs: + raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") + register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) + + # check for local time + if 'localDate' in kwargs: + raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") + if 'localTime' in kwargs: + raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") + + # check for a minimum value + if kwargs['maxApduLengthAccepted'] < 50: + raise ValueError("invalid max APDU length accepted") + + # dump the updated attributes + if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) + + # proceed as usual + DeviceObject.__init__(self, **kwargs) + + # create a default implementation of an object list for local devices. + # If it is specified in the kwargs, that overrides this default. + if ('objectList' not in kwargs): + self.objectList = ArrayOf(ObjectIdentifier)([self.objectIdentifier]) + + # if the object has a property list and one wasn't provided + # in the kwargs, then it was created by default and the objectList + # property should be included + if ('propertyList' not in kwargs) and self.propertyList: + # make sure it's not already there + if 'objectList' not in self.propertyList: + self.propertyList.append('objectList') + +# +# Who-Is I-Am Services +# + +@bacpypes_debugging +class WhoIsIAmServices(Capability): + + def __init__(self): + if _debug: WhoIsIAmServices._debug("__init__") + Capability.__init__(self) + + def who_is(self, low_limit=None, high_limit=None, address=None): + if _debug: WhoIsIAmServices._debug("who_is") + + # build a request + whoIs = WhoIsRequest() + + # defaults to a global broadcast + if not address: + address = GlobalBroadcast() + + # set the destination + whoIs.pduDestination = address + + # check for consistent parameters + if (low_limit is not None): + if (high_limit is None): + raise MissingRequiredParameter("high_limit required") + if (low_limit < 0) or (low_limit > 4194303): + raise ParameterOutOfRange("low_limit out of range") + + # low limit is fine + whoIs.deviceInstanceRangeLowLimit = low_limit + + if (high_limit is not None): + if (low_limit is None): + raise MissingRequiredParameter("low_limit required") + if (high_limit < 0) or (high_limit > 4194303): + raise ParameterOutOfRange("high_limit out of range") + + # high limit is fine + whoIs.deviceInstanceRangeHighLimit = high_limit + + if _debug: WhoIsIAmServices._debug(" - whoIs: %r", whoIs) + + ### put the parameters someplace where they can be matched when the + ### appropriate I-Am comes in + + # away it goes + self.request(whoIs) + + def do_WhoIsRequest(self, apdu): + """Respond to a Who-Is request.""" + if _debug: WhoIsIAmServices._debug("do_WhoIsRequest %r", apdu) + + # ignore this if there's no local device + if not self.localDevice: + if _debug: WhoIsIAmServices._debug(" - no local device") + return + + # extract the parameters + low_limit = apdu.deviceInstanceRangeLowLimit + high_limit = apdu.deviceInstanceRangeHighLimit + + # check for consistent parameters + if (low_limit is not None): + if (high_limit is None): + raise MissingRequiredParameter("deviceInstanceRangeHighLimit required") + if (low_limit < 0) or (low_limit > 4194303): + raise ParameterOutOfRange("deviceInstanceRangeLowLimit out of range") + if (high_limit is not None): + if (low_limit is None): + raise MissingRequiredParameter("deviceInstanceRangeLowLimit required") + if (high_limit < 0) or (high_limit > 4194303): + raise ParameterOutOfRange("deviceInstanceRangeHighLimit out of range") + + # see we should respond + if (low_limit is not None): + if (self.localDevice.objectIdentifier[1] < low_limit): + return + if (high_limit is not None): + if (self.localDevice.objectIdentifier[1] > high_limit): + return + + # generate an I-Am + self.i_am(address=apdu.pduSource) + + def i_am(self, address=None): + if _debug: WhoIsIAmServices._debug("i_am") + + # this requires a local device + if not self.localDevice: + if _debug: WhoIsIAmServices._debug(" - no local device") + return + + # create a I-Am "response" back to the source + iAm = IAmRequest( + iAmDeviceIdentifier=self.localDevice.objectIdentifier, + maxAPDULengthAccepted=self.localDevice.maxApduLengthAccepted, + segmentationSupported=self.localDevice.segmentationSupported, + vendorID=self.localDevice.vendorIdentifier, + ) + + # defaults to a global broadcast + if not address: + address = GlobalBroadcast() + iAm.pduDestination = address + if _debug: WhoIsIAmServices._debug(" - iAm: %r", iAm) + + # away it goes + self.request(iAm) + + def do_IAmRequest(self, apdu): + """Respond to an I-Am request.""" + if _debug: WhoIsIAmServices._debug("do_IAmRequest %r", apdu) + + # check for required parameters + if apdu.iAmDeviceIdentifier is None: + raise MissingRequiredParameter("iAmDeviceIdentifier required") + if apdu.maxAPDULengthAccepted is None: + raise MissingRequiredParameter("maxAPDULengthAccepted required") + if apdu.segmentationSupported is None: + raise MissingRequiredParameter("segmentationSupported required") + if apdu.vendorID is None: + raise MissingRequiredParameter("vendorID required") + + # extract the device instance number + device_instance = apdu.iAmDeviceIdentifier[1] + if _debug: WhoIsIAmServices._debug(" - device_instance: %r", device_instance) + + # extract the source address + device_address = apdu.pduSource + if _debug: WhoIsIAmServices._debug(" - device_address: %r", device_address) + + ### check to see if the application is looking for this device + ### and update the device info cache if it is + +# +# Who-Has I-Have Services +# + +@bacpypes_debugging +class WhoHasIHaveServices(Capability): + + def __init__(self): + if _debug: WhoHasIHaveServices._debug("__init__") + Capability.__init__(self) + + def who_has(self, thing, address=None): + if _debug: WhoHasIHaveServices._debug("who_has %r address=%r", thing, address) + + raise NotImplementedError("who_has") + + def do_WhoHasRequest(self, apdu): + """Respond to a Who-Has request.""" + if _debug: WhoHasIHaveServices._debug("do_WhoHasRequest, %r", apdu) + + # ignore this if there's no local device + if not self.localDevice: + if _debug: WhoIsIAmServices._debug(" - no local device") + return + + # find the object + if apdu.object.objectIdentifier is not None: + obj = self.objectIdentifier.get(apdu.object.objectIdentifier, None) + elif apdu.object.objectName is not None: + obj = self.objectName.get(apdu.object.objectName, None) + else: + raise InconsistentParameters("object identifier or object name required") + if not obj: + raise ExecutionError(errorClass='object', errorCode='unknownObject') + + # send out the response + self.i_have(obj, address=apdu.pduSource) + + def i_have(self, thing, address=None): + if _debug: WhoHasIHaveServices._debug("i_have %r address=%r", thing, address) + + # ignore this if there's no local device + if not self.localDevice: + if _debug: WhoIsIAmServices._debug(" - no local device") + return + + # build the request + iHave = IHaveRequest( + deviceIdentifier=self.localDevice.objectIdentifier, + objectIdentifier=thing.objectIdentifier, + objectName=thing.objectName, + ) + + # defaults to a global broadcast + if not address: + address = GlobalBroadcast() + iHave.pduDestination = address + if _debug: WhoHasIHaveServices._debug(" - iHave: %r", iHave) + + # send it along + self.request(iHave) + + def do_IHaveRequest(self, apdu): + """Respond to a I-Have request.""" + if _debug: WhoHasIHaveServices._debug("do_IHaveRequest %r", apdu) + + # check for required parameters + if apdu.deviceIdentifier is None: + raise MissingRequiredParameter("deviceIdentifier required") + if apdu.objectIdentifier is None: + raise MissingRequiredParameter("objectIdentifier required") + if apdu.objectName is None: + raise MissingRequiredParameter("objectName required") + + ### check to see if the application is looking for this object diff --git a/py27/bacpypes/service/file.py b/py27/bacpypes/service/file.py new file mode 100644 index 00000000..8723b6ff --- /dev/null +++ b/py27/bacpypes/service/file.py @@ -0,0 +1,297 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger +from ..capability import Capability + +from ..object import FileObject + +from ..apdu import AtomicReadFileACK, AtomicReadFileACKAccessMethodChoice, \ + AtomicReadFileACKAccessMethodRecordAccess, \ + AtomicReadFileACKAccessMethodStreamAccess, \ + AtomicWriteFileACK +from ..errors import ExecutionError, MissingRequiredParameter + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# Local Record Access File Object Type +# + +@bacpypes_debugging +class LocalRecordAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a record accessed file object. """ + if _debug: + LocalRecordAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'recordAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'recordAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of records. """ + raise NotImplementedError("__len__") + + def read_record(self, start_record, record_count): + """ Read a number of records starting at a specific record. """ + raise NotImplementedError("read_record") + + def write_record(self, start_record, record_count, record_data): + """ Write a number of records, starting at a specific record. """ + raise NotImplementedError("write_record") + +# +# Local Stream Access File Object Type +# + +@bacpypes_debugging +class LocalStreamAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a stream accessed file object. """ + if _debug: + LocalStreamAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'streamAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'streamAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of octets in the file. """ + raise NotImplementedError("write_file") + + def read_stream(self, start_position, octet_count): + """ Read a chunk of data out of the file. """ + raise NotImplementedError("read_stream") + + def write_stream(self, start_position, data): + """ Write a number of octets, starting at a specific offset. """ + raise NotImplementedError("write_stream") + +# +# File Application Mixin +# + +@bacpypes_debugging +class FileServices(Capability): + + def __init__(self): + if _debug: FileServices._debug("__init__") + Capability.__init__(self) + + def do_AtomicReadFileRequest(self, apdu): + """Return one of our records.""" + if _debug: FileServices._debug("do_AtomicReadFileRequest %r", apdu) + + if (apdu.fileIdentifier[0] != 'file'): + raise ExecutionError('services', 'inconsistentObjectType') + + # get the object + obj = self.get_object_id(apdu.fileIdentifier) + if _debug: FileServices._debug(" - object: %r", obj) + + if not obj: + raise ExecutionError('object', 'unknownObject') + + if apdu.accessMethod.recordAccess: + # check against the object + if obj.fileAccessMethod != 'recordAccess': + raise ExecutionError('services', 'invalidFileAccessMethod') + + # simplify + record_access = apdu.accessMethod.recordAccess + + # check for required parameters + if record_access.fileStartRecord is None: + raise MissingRequiredParameter("fileStartRecord required") + if record_access.requestedRecordCount is None: + raise MissingRequiredParameter("requestedRecordCount required") + + ### verify start is valid - double check this (empty files?) + if (record_access.fileStartRecord < 0) or \ + (record_access.fileStartRecord >= len(obj)): + raise ExecutionError('services', 'invalidFileStartPosition') + + # pass along to the object + end_of_file, record_data = obj.read_record( + record_access.fileStartRecord, + record_access.requestedRecordCount, + ) + if _debug: FileServices._debug(" - record_data: %r", record_data) + + # this is an ack + resp = AtomicReadFileACK(context=apdu, + endOfFile=end_of_file, + accessMethod=AtomicReadFileACKAccessMethodChoice( + recordAccess=AtomicReadFileACKAccessMethodRecordAccess( + fileStartRecord=record_access.fileStartRecord, + returnedRecordCount=len(record_data), + fileRecordData=record_data, + ), + ), + ) + + elif apdu.accessMethod.streamAccess: + # check against the object + if obj.fileAccessMethod != 'streamAccess': + raise ExecutionError('services', 'invalidFileAccessMethod') + + # simplify + stream_access = apdu.accessMethod.streamAccess + + # check for required parameters + if stream_access.fileStartPosition is None: + raise MissingRequiredParameter("fileStartPosition required") + if stream_access.requestedOctetCount is None: + raise MissingRequiredParameter("requestedOctetCount required") + + ### verify start is valid - double check this (empty files?) + if (stream_access.fileStartPosition < 0) or \ + (stream_access.fileStartPosition >= len(obj)): + raise ExecutionError('services', 'invalidFileStartPosition') + + # pass along to the object + end_of_file, record_data = obj.read_stream( + stream_access.fileStartPosition, + stream_access.requestedOctetCount, + ) + if _debug: FileServices._debug(" - record_data: %r", record_data) + + # this is an ack + resp = AtomicReadFileACK(context=apdu, + endOfFile=end_of_file, + accessMethod=AtomicReadFileACKAccessMethodChoice( + streamAccess=AtomicReadFileACKAccessMethodStreamAccess( + fileStartPosition=stream_access.fileStartPosition, + fileData=record_data, + ), + ), + ) + + if _debug: FileServices._debug(" - resp: %r", resp) + + # return the result + self.response(resp) + + def do_AtomicWriteFileRequest(self, apdu): + """Return one of our records.""" + if _debug: FileServices._debug("do_AtomicWriteFileRequest %r", apdu) + + if (apdu.fileIdentifier[0] != 'file'): + raise ExecutionError('services', 'inconsistentObjectType') + + # get the object + obj = self.get_object_id(apdu.fileIdentifier) + if _debug: FileServices._debug(" - object: %r", obj) + + if not obj: + raise ExecutionError('object', 'unknownObject') + + if apdu.accessMethod.recordAccess: + # check against the object + if obj.fileAccessMethod != 'recordAccess': + raise ExecutionError('services', 'invalidFileAccessMethod') + + # simplify + record_access = apdu.accessMethod.recordAccess + + # check for required parameters + if record_access.fileStartRecord is None: + raise MissingRequiredParameter("fileStartRecord required") + if record_access.recordCount is None: + raise MissingRequiredParameter("recordCount required") + if record_access.fileRecordData is None: + raise MissingRequiredParameter("fileRecordData required") + + # check for read-only + if obj.readOnly: + raise ExecutionError('services', 'fileAccessDenied') + + # pass along to the object + start_record = obj.write_record( + record_access.fileStartRecord, + record_access.recordCount, + record_access.fileRecordData, + ) + if _debug: FileServices._debug(" - start_record: %r", start_record) + + # this is an ack + resp = AtomicWriteFileACK(context=apdu, + fileStartRecord=start_record, + ) + + elif apdu.accessMethod.streamAccess: + # check against the object + if obj.fileAccessMethod != 'streamAccess': + raise ExecutionError('services', 'invalidFileAccessMethod') + + # simplify + stream_access = apdu.accessMethod.streamAccess + + # check for required parameters + if stream_access.fileStartPosition is None: + raise MissingRequiredParameter("fileStartPosition required") + if stream_access.fileData is None: + raise MissingRequiredParameter("fileData required") + + # check for read-only + if obj.readOnly: + raise ExecutionError('services', 'fileAccessDenied') + + # pass along to the object + start_position = obj.write_stream( + stream_access.fileStartPosition, + stream_access.fileData, + ) + if _debug: FileServices._debug(" - start_position: %r", start_position) + + # this is an ack + resp = AtomicWriteFileACK(context=apdu, + fileStartPosition=start_position, + ) + + if _debug: FileServices._debug(" - resp: %r", resp) + + # return the result + self.response(resp) + +# +# FileServicesClient +# + +class FileServicesClient(Capability): + + def read_record(self, address, fileIdentifier, start_record, record_count): + """ Read a number of records starting at a specific record. """ + raise NotImplementedError("read_record") + + def write_record(self, address, fileIdentifier, start_record, record_count, record_data): + """ Write a number of records, starting at a specific record. """ + raise NotImplementedError("write_record") + + def read_stream(self, address, fileIdentifier, start_position, octet_count): + """ Read a chunk of data out of the file. """ + raise NotImplementedError("read_stream") + + def write_stream(self, address, fileIdentifier, start_position, data): + """ Write a number of octets, starting at a specific offset. """ + raise NotImplementedError("write_stream") diff --git a/py27/bacpypes/service/object.py b/py27/bacpypes/service/object.py new file mode 100644 index 00000000..e453a5b7 --- /dev/null +++ b/py27/bacpypes/service/object.py @@ -0,0 +1,327 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger +from ..capability import Capability + +from ..basetypes import ErrorType +from ..primitivedata import Atomic, Null, Unsigned +from ..constructeddata import Any, Array + +from ..apdu import Error, \ + SimpleAckPDU, ReadPropertyACK, ReadPropertyMultipleACK, \ + ReadAccessResult, ReadAccessResultElement, ReadAccessResultElementChoice +from ..errors import ExecutionError +from ..object import PropertyError + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# ReadProperty and WriteProperty Services +# + +@bacpypes_debugging +class ReadWritePropertyServices(Capability): + + def __init__(self): + if _debug: ReadWritePropertyServices._debug("__init__") + Capability.__init__(self) + + def do_ReadPropertyRequest(self, apdu): + """Return the value of some property of one of our objects.""" + if _debug: ReadWritePropertyServices._debug("do_ReadPropertyRequest %r", apdu) + + # extract the object identifier + objId = apdu.objectIdentifier + + # check for wildcard + if (objId == ('device', 4194303)) and self.localDevice is not None: + if _debug: ReadWritePropertyServices._debug(" - wildcard device identifier") + objId = self.localDevice.objectIdentifier + + # get the object + obj = self.get_object_id(objId) + if _debug: ReadWritePropertyServices._debug(" - object: %r", obj) + + if not obj: + raise ExecutionError(errorClass='object', errorCode='unknownObject') + + try: + # get the datatype + datatype = obj.get_datatype(apdu.propertyIdentifier) + if _debug: ReadWritePropertyServices._debug(" - datatype: %r", datatype) + + # get the value + value = obj.ReadProperty(apdu.propertyIdentifier, apdu.propertyArrayIndex) + if _debug: ReadWritePropertyServices._debug(" - value: %r", value) + if value is None: + raise PropertyError(apdu.propertyIdentifier) + + # change atomic values into something encodeable + if issubclass(datatype, Atomic): + value = datatype(value) + elif issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): + if apdu.propertyArrayIndex == 0: + value = Unsigned(value) + elif issubclass(datatype.subtype, Atomic): + value = datatype.subtype(value) + elif not isinstance(value, datatype.subtype): + raise TypeError("invalid result datatype, expecting {0} and got {1}" \ + .format(datatype.subtype.__name__, type(value).__name__)) + elif not isinstance(value, datatype): + raise TypeError("invalid result datatype, expecting {0} and got {1}" \ + .format(datatype.__name__, type(value).__name__)) + if _debug: ReadWritePropertyServices._debug(" - encodeable value: %r", value) + + # this is a ReadProperty ack + resp = ReadPropertyACK(context=apdu) + resp.objectIdentifier = objId + resp.propertyIdentifier = apdu.propertyIdentifier + resp.propertyArrayIndex = apdu.propertyArrayIndex + + # save the result in the property value + resp.propertyValue = Any() + resp.propertyValue.cast_in(value) + if _debug: ReadWritePropertyServices._debug(" - resp: %r", resp) + + except PropertyError: + raise ExecutionError(errorClass='object', errorCode='unknownProperty') + + # return the result + self.response(resp) + + def do_WritePropertyRequest(self, apdu): + """Change the value of some property of one of our objects.""" + if _debug: ReadWritePropertyServices._debug("do_WritePropertyRequest %r", apdu) + + # get the object + obj = self.get_object_id(apdu.objectIdentifier) + if _debug: ReadWritePropertyServices._debug(" - object: %r", obj) + if not obj: + raise ExecutionError(errorClass='object', errorCode='unknownObject') + + try: + # check if the property exists + if obj.ReadProperty(apdu.propertyIdentifier, apdu.propertyArrayIndex) is None: + raise PropertyError(apdu.propertyIdentifier) + + # get the datatype, special case for null + if apdu.propertyValue.is_application_class_null(): + datatype = Null + else: + datatype = obj.get_datatype(apdu.propertyIdentifier) + if _debug: ReadWritePropertyServices._debug(" - datatype: %r", datatype) + + # special case for array parts, others are managed by cast_out + if issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): + if apdu.propertyArrayIndex == 0: + value = apdu.propertyValue.cast_out(Unsigned) + else: + value = apdu.propertyValue.cast_out(datatype.subtype) + else: + value = apdu.propertyValue.cast_out(datatype) + if _debug: ReadWritePropertyServices._debug(" - value: %r", value) + + # change the value + value = obj.WriteProperty(apdu.propertyIdentifier, value, apdu.propertyArrayIndex, apdu.priority) + + # success + resp = SimpleAckPDU(context=apdu) + if _debug: ReadWritePropertyServices._debug(" - resp: %r", resp) + + except PropertyError: + raise ExecutionError(errorClass='object', errorCode='unknownProperty') + + # return the result + self.response(resp) + +# +# read_property_to_any +# + +@bacpypes_debugging +def read_property_to_any(obj, propertyIdentifier, propertyArrayIndex=None): + """Read the specified property of the object, with the optional array index, + and cast the result into an Any object.""" + if _debug: read_property_to_any._debug("read_property_to_any %s %r %r", obj, propertyIdentifier, propertyArrayIndex) + + # get the datatype + datatype = obj.get_datatype(propertyIdentifier) + if _debug: read_property_to_any._debug(" - datatype: %r", datatype) + if datatype is None: + raise ExecutionError(errorClass='property', errorCode='datatypeNotSupported') + + # get the value + value = obj.ReadProperty(propertyIdentifier, propertyArrayIndex) + if _debug: read_property_to_any._debug(" - value: %r", value) + if value is None: + raise ExecutionError(errorClass='property', errorCode='unknownProperty') + + # change atomic values into something encodeable + if issubclass(datatype, Atomic): + value = datatype(value) + elif issubclass(datatype, Array) and (propertyArrayIndex is not None): + if propertyArrayIndex == 0: + value = Unsigned(value) + elif issubclass(datatype.subtype, Atomic): + value = datatype.subtype(value) + elif not isinstance(value, datatype.subtype): + raise TypeError("invalid result datatype, expecting %s and got %s" \ + % (datatype.subtype.__name__, type(value).__name__)) + elif not isinstance(value, datatype): + raise TypeError("invalid result datatype, expecting %s and got %s" \ + % (datatype.__name__, type(value).__name__)) + if _debug: read_property_to_any._debug(" - encodeable value: %r", value) + + # encode the value + result = Any() + result.cast_in(value) + if _debug: read_property_to_any._debug(" - result: %r", result) + + # return the object + return result + +# +# read_property_to_result_element +# + +@bacpypes_debugging +def read_property_to_result_element(obj, propertyIdentifier, propertyArrayIndex=None): + """Read the specified property of the object, with the optional array index, + and cast the result into an Any object.""" + if _debug: read_property_to_result_element._debug("read_property_to_result_element %s %r %r", obj, propertyIdentifier, propertyArrayIndex) + + # save the result in the property value + read_result = ReadAccessResultElementChoice() + + try: + read_result.propertyValue = read_property_to_any(obj, propertyIdentifier, propertyArrayIndex) + if _debug: read_property_to_result_element._debug(" - success") + except PropertyError as error: + if _debug: read_property_to_result_element._debug(" - error: %r", error) + read_result.propertyAccessError = ErrorType(errorClass='property', errorCode='unknownProperty') + except ExecutionError as error: + if _debug: read_property_to_result_element._debug(" - error: %r", error) + read_result.propertyAccessError = ErrorType(errorClass=error.errorClass, errorCode=error.errorCode) + + # make an element for this value + read_access_result_element = ReadAccessResultElement( + propertyIdentifier=propertyIdentifier, + propertyArrayIndex=propertyArrayIndex, + readResult=read_result, + ) + if _debug: read_property_to_result_element._debug(" - read_access_result_element: %r", read_access_result_element) + + # fini + return read_access_result_element + +# +# ReadWritePropertyMultipleServices +# + +@bacpypes_debugging +class ReadWritePropertyMultipleServices(Capability): + + def __init__(self): + if _debug: ReadWritePropertyMultipleServices._debug("__init__") + Capability.__init__(self) + + def do_ReadPropertyMultipleRequest(self, apdu): + """Respond to a ReadPropertyMultiple Request.""" + if _debug: ReadWritePropertyMultipleServices._debug("do_ReadPropertyMultipleRequest %r", apdu) + + # response is a list of read access results (or an error) + resp = None + read_access_result_list = [] + + # loop through the request + for read_access_spec in apdu.listOfReadAccessSpecs: + # get the object identifier + objectIdentifier = read_access_spec.objectIdentifier + if _debug: ReadWritePropertyMultipleServices._debug(" - objectIdentifier: %r", objectIdentifier) + + # check for wildcard + if (objectIdentifier == ('device', 4194303)) and self.localDevice is not None: + if _debug: ReadWritePropertyMultipleServices._debug(" - wildcard device identifier") + objectIdentifier = self.localDevice.objectIdentifier + + # get the object + obj = self.get_object_id(objectIdentifier) + if _debug: ReadWritePropertyMultipleServices._debug(" - object: %r", obj) + + # make sure it exists + if not obj: + resp = Error(errorClass='object', errorCode='unknownObject', context=apdu) + if _debug: ReadWritePropertyMultipleServices._debug(" - unknown object error: %r", resp) + break + + # build a list of result elements + read_access_result_element_list = [] + + # loop through the property references + for prop_reference in read_access_spec.listOfPropertyReferences: + # get the property identifier + propertyIdentifier = prop_reference.propertyIdentifier + if _debug: ReadWritePropertyMultipleServices._debug(" - propertyIdentifier: %r", propertyIdentifier) + + # get the array index (optional) + propertyArrayIndex = prop_reference.propertyArrayIndex + if _debug: ReadWritePropertyMultipleServices._debug(" - propertyArrayIndex: %r", propertyArrayIndex) + + # check for special property identifiers + if propertyIdentifier in ('all', 'required', 'optional'): + for propId, prop in obj._properties.items(): + if _debug: ReadWritePropertyMultipleServices._debug(" - checking: %r %r", propId, prop.optional) + + if (propertyIdentifier == 'all'): + pass + elif (propertyIdentifier == 'required') and (prop.optional): + if _debug: ReadWritePropertyMultipleServices._debug(" - not a required property") + continue + elif (propertyIdentifier == 'optional') and (not prop.optional): + if _debug: ReadWritePropertyMultipleServices._debug(" - not an optional property") + continue + + # read the specific property + read_access_result_element = read_property_to_result_element(obj, propId, propertyArrayIndex) + + # check for undefined property + if read_access_result_element.readResult.propertyAccessError \ + and read_access_result_element.readResult.propertyAccessError.errorCode == 'unknownProperty': + continue + + # add it to the list + read_access_result_element_list.append(read_access_result_element) + + else: + # read the specific property + read_access_result_element = read_property_to_result_element(obj, propertyIdentifier, propertyArrayIndex) + + # add it to the list + read_access_result_element_list.append(read_access_result_element) + + # build a read access result + read_access_result = ReadAccessResult( + objectIdentifier=objectIdentifier, + listOfResults=read_access_result_element_list + ) + if _debug: ReadWritePropertyMultipleServices._debug(" - read_access_result: %r", read_access_result) + + # add it to the list + read_access_result_list.append(read_access_result) + + # this is a ReadPropertyMultiple ack + if not resp: + resp = ReadPropertyMultipleACK(context=apdu) + resp.listOfReadAccessResults = read_access_result_list + if _debug: ReadWritePropertyMultipleServices._debug(" - resp: %r", resp) + + # return the result + self.response(resp) + +# def do_WritePropertyMultipleRequest(self, apdu): +# """Respond to a WritePropertyMultiple Request.""" +# if _debug: ReadWritePropertyMultipleServices._debug("do_ReadPropertyMultipleRequest %r", apdu) +# +# raise NotImplementedError() diff --git a/py27/bacpypes/service/test.py b/py27/bacpypes/service/test.py new file mode 100644 index 00000000..fb075dda --- /dev/null +++ b/py27/bacpypes/service/test.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +""" +Test Service +""" + +from ..debugging import bacpypes_debugging, ModuleLogger + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +@bacpypes_debugging +def some_function(*args): + if _debug: some_function._debug("f %r", args) + + return args[0] + 1 + diff --git a/py27/bacpypes/tcp.py b/py27/bacpypes/tcp.py index 574178a6..6131916b 100755 --- a/py27/bacpypes/tcp.py +++ b/py27/bacpypes/tcp.py @@ -6,6 +6,7 @@ import asyncore import socket + import cPickle as pickle from time import time as _time, sleep as _sleep from StringIO import StringIO @@ -102,68 +103,111 @@ def __init__(self, peer): self.peer = peer # create a request buffer - self.request = '' + self.request = b'' + + # try to connect + try: + if _debug: TCPClient._debug(" - initiate connection") + self.connect(peer) + except socket.error as err: + if _debug: TCPClient._debug(" - connect socket error: %r", err) - # hold the socket error if there was one - self.socketError = None + # pass along to a handler + self.handle_error(err) - # try to connect the socket - if _debug: TCPClient._debug(" - try to connect") - self.connect(peer) - if _debug: TCPClient._debug(" - connected (maybe)") + def handle_accept(self): + if _debug: TCPClient._debug("handle_accept") def handle_connect(self): - if _debug: deferred(TCPClient._debug, "handle_connect") + if _debug: TCPClient._debug("handle_connect") + + def handle_connect_event(self): + if _debug: TCPClient._debug("handle_connect_event") - def handle_expt(self): - pass + # there might be an error + err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + if _debug: TCPClient._debug(" - err: %r", err) + + # check for connection refused + if (err == 0): + if _debug: TCPClient._debug(" - no error") + elif (err == 111): + if _debug: TCPClient._debug(" - connection to %r refused", self.peer) + self.handle_error(socket.error(111, "connection refused")) + return + + # pass along + asyncore.dispatcher.handle_connect_event(self) def readable(self): - return 1 + return self.connected def handle_read(self): - if _debug: deferred(TCPClient._debug, "handle_read") + if _debug: TCPClient._debug("handle_read") try: msg = self.recv(65536) - if _debug: deferred(TCPClient._debug, " - received %d octets", len(msg)) - self.socketError = None + if _debug: TCPClient._debug(" - received %d octets", len(msg)) # no socket means it was closed if not self.socket: - if _debug: deferred(TCPClient._debug, " - socket was closed") + if _debug: TCPClient._debug(" - socket was closed") else: - # sent the data upstream + # send the data upstream deferred(self.response, PDU(msg)) except socket.error as err: - if (err.args[0] == 111): - deferred(TCPClient._error, "connection to %r refused", self.peer) + if (err.args[0] in (61, 111)): + if _debug: TCPClient._debug(" - connection to %r refused", self.peer) else: - deferred(TCPClient._error, "TCPClient.handle_read socket error: %r", err) - self.socketError = err + if _debug: TCPClient._debug(" - recv socket error: %r", err) + + # pass along to a handler + self.handle_error(err) def writable(self): return (len(self.request) != 0) def handle_write(self): - if _debug: deferred(TCPClient._debug, "handle_write") + if _debug: TCPClient._debug("handle_write") try: sent = self.send(self.request) - if _debug: deferred(TCPClient._debug, " - sent %d octets, %d remaining", sent, len(self.request) - sent) - self.socketError = None + if _debug: TCPClient._debug(" - sent %d octets, %d remaining", sent, len(self.request) - sent) self.request = self.request[sent:] + except socket.error as err: - if (err.args[0] == 111): - deferred(TCPClient._error, "connection to %r refused", self.peer) + if (err.args[0] == 32): + if _debug: TCPClient._debug(" - broken pipe to %r", self.peer) + return + elif (err.args[0] in (61, 111)): + if _debug: TCPClient._debug(" - connection to %r refused", self.peer) else: - deferred(TCPClient._error, "handle_write socket error: %s", err) - self.socketError = err + if _debug: TCPClient._debug(" - send socket error: %s", err) + + # pass along to a handler + self.handle_error(err) + + def handle_write_event(self): + if _debug: TCPClient._debug("handle_write_event") + + # there might be an error + err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + if _debug: TCPClient._debug(" - err: %r", err) + + # check for connection refused + if (err in (61, 111)): + if _debug: TCPClient._debug(" - connection to %r refused", self.peer) + self.handle_error(socket.error(err, "connection refused")) + self.handle_close() + return + + # pass along + asyncore.dispatcher.handle_write_event(self) def handle_close(self): - if _debug: deferred(TCPClient._debug, "handle_close") + if _debug: TCPClient._debug("handle_close") # close the socket self.close() @@ -171,6 +215,20 @@ def handle_close(self): # make sure other routines know the socket is closed self.socket = None + def handle_error(self, error=None): + """Trap for TCPClient errors, otherwise continue.""" + if _debug: TCPClient._debug("handle_error %r", error) + + # core does not take parameters + asyncore.dispatcher.handle_error(self) + + def handle_error(self, error=None): + """Trap for TCPClient errors, otherwise continue.""" + if _debug: TCPClient._debug("handle_error %r", error) + + # core does not take parameters + asyncore.dispatcher.handle_error(self) + def indication(self, pdu): """Requests are queued for delivery.""" if _debug: TCPClient._debug("indication %r", pdu) @@ -208,6 +266,16 @@ def __init__(self, director, peer): # tell the director this is a new actor self.director.add_actor(self) + def handle_error(self, error=None): + """Trap for TCPClient errors, otherwise continue.""" + if _debug: TCPClientActor._debug("handle_error %r", error) + + # pass along to the director + if error is not None: + self.director.actor_error(self, error) + else: + TCPClient.handle_error(self) + def handle_close(self): if _debug: TCPClientActor._debug("handle_close") @@ -220,7 +288,7 @@ def handle_close(self): self.timer.suspend_task() # tell the director this is gone - self.director.remove_actor(self) + self.director.del_actor(self) # pass the function along TCPClient.handle_close(self) @@ -322,23 +390,30 @@ def add_actor(self, actor): # tell the ASE there is a new client if self.serviceElement: - self.sap_request(addPeer=actor.peer) + self.sap_request(add_actor=actor) - def remove_actor(self, actor): + def del_actor(self, actor): """Remove an actor when the socket is closed.""" - if _debug: TCPClientDirector._debug("remove_actor %r", actor) + if _debug: TCPClientDirector._debug("del_actor %r", actor) del self.clients[actor.peer] # tell the ASE the client has gone away if self.serviceElement: - self.sap_request(delPeer=actor.peer) + self.sap_request(del_actor=actor) # see if it should be reconnected if actor.peer in self.reconnect: connect_task = FunctionTask(self.connect, actor.peer) connect_task.install_task(_time() + self.reconnect[actor.peer]) + def actor_error(self, actor, error): + if _debug: TCPClientDirector._debug("actor_error %r %r", actor, error) + + # tell the ASE the actor had an error + if self.serviceElement: + self.sap_request(actor_error=actor, error=error) + def get_actor(self, address): """ Get the actor associated with an address or None. """ return self.clients.get(address, None) @@ -399,70 +474,78 @@ def __init__(self, sock, peer): self.peer = peer # create a request buffer - self.request = '' - - # hold the socket error if there was one - self.socketError = None + self.request = b'' def handle_connect(self): - if _debug: deferred(TCPServer._debug, "handle_connect") + if _debug: TCPServer._debug("handle_connect") def readable(self): - return 1 + return self.connected def handle_read(self): - if _debug: deferred(TCPServer._debug, "handle_read") + if _debug: TCPServer._debug("handle_read") try: msg = self.recv(65536) - if _debug: deferred(TCPServer._debug, " - received %d octets", len(msg)) - self.socketError = None + if _debug: TCPServer._debug(" - received %d octets", len(msg)) # no socket means it was closed if not self.socket: - if _debug: deferred(TCPServer._debug, " - socket was closed") + if _debug: TCPServer._debug(" - socket was closed") else: + # send the data upstream deferred(self.response, PDU(msg)) except socket.error as err: if (err.args[0] == 111): - deferred(TCPServer._error, "connection to %r refused", self.peer) + if _debug: TCPServer._debug(" - connection to %r refused", self.peer) else: - deferred(TCPServer._error, "handle_read socket error: %s", err) - self.socketError = err + if _debug: TCPServer._debug(" - recv socket error: %s", err) + + # pass along to a handler + self.handle_error(err) def writable(self): return (len(self.request) != 0) def handle_write(self): - if _debug: deferred(TCPServer._debug, "handle_write") + if _debug: TCPServer._debug("handle_write") try: sent = self.send(self.request) - if _debug: deferred(TCPServer._debug, " - sent %d octets, %d remaining", sent, len(self.request) - sent) - self.socketError = None + if _debug: TCPServer._debug(" - sent %d octets, %d remaining", sent, len(self.request) - sent) self.request = self.request[sent:] - except socket.error as why: - if (why.args[0] == 111): - deferred(TCPServer._error, "connection to %r refused", self.peer) + + except socket.error as err: + if (err.args[0] == 111): + if _debug: TCPServer._debug(" - connection to %r refused", self.peer) else: - deferred(TCPServer._error, "handle_write socket error: %s", why) - self.socketError = why + if _debug: TCPServer._debug(" - send socket error: %s", err) + + # sent the exception upstream + self.handle_error(err) def handle_close(self): - if _debug: deferred(TCPServer._debug, "handle_close") + if _debug: TCPServer._debug("handle_close") if not self: - deferred(TCPServer._warning, "handle_close: self is None") + if _debug: TCPServer._debug(" - self is None") return if not self.socket: - deferred(TCPServer._warning, "handle_close: socket already closed") + if _debug: TCPServer._debug(" - socket already closed") return self.close() self.socket = None + def handle_error(self, error=None): + """Trap for TCPServer errors, otherwise continue.""" + if _debug: TCPServer._debug("handle_error %r", error) + + # core does not take parameters + asyncore.dispatcher.handle_error(self) + def indication(self, pdu): """Requests are queued for delivery.""" if _debug: TCPServer._debug("indication %r", pdu) @@ -497,6 +580,16 @@ def __init__(self, director, sock, peer): # tell the director this is a new actor self.director.add_actor(self) + def handle_error(self, error=None): + """Trap for TCPServer errors, otherwise continue.""" + if _debug: TCPServerActor._debug("handle_error %r", error) + + # pass along to the director + if error is not None: + self.director.actor_error(self, error) + else: + TCPServer.handle_error(self) + def handle_close(self): if _debug: TCPServerActor._debug("handle_close") @@ -505,7 +598,7 @@ def handle_close(self): self.flushTask.suspend_task() # tell the director this is gone - self.director.remove_actor(self) + self.director.del_actor(self) # pass it down TCPServer.handle_close(self) @@ -662,19 +755,26 @@ def add_actor(self, actor): # tell the ASE there is a new server if self.serviceElement: - self.sap_request(addPeer=actor.peer) + self.sap_request(add_actor=actor) - def remove_actor(self, actor): - if _debug: TCPServerDirector._debug("remove_actor %r", actor) + def del_actor(self, actor): + if _debug: TCPServerDirector._debug("del_actor %r", actor) try: del self.servers[actor.peer] except KeyError: - TCPServerDirector._warning("remove_actor: %r not an actor", actor) + TCPServerDirector._warning("del_actor: %r not an actor", actor) # tell the ASE the server has gone away if self.serviceElement: - self.sap_request(delPeer=actor.peer) + self.sap_request(del_actor=actor) + + def actor_error(self, actor, error): + if _debug: TCPServerDirector._debug("actor_error %r %r", actor, error) + + # tell the ASE the actor had an error + if self.serviceElement: + self.sap_request(actor_error=actor, error=error) def get_actor(self, address): """ Get the actor associated with an address or None. """ @@ -780,20 +880,23 @@ def __init__(self, stp, aseID=None, sapID=None): # save a reference to the StreamToPacket object self.stp = stp - def indication(self, addPeer=None, delPeer=None): - if _debug: StreamToPacketSAP._debug("indication addPeer=%r delPeer=%r", addPeer, delPeer) + def indication(self, add_actor=None, del_actor=None, actor_error=None, error=None): + if _debug: StreamToPacketSAP._debug("indication add_actor=%r del_actor=%r", add_actor, del_actor) - if addPeer: + if add_actor: # create empty buffers associated with the peer - self.stp.upstreamBuffer[addPeer] = b'' - self.stp.downstreamBuffer[addPeer] = b'' + self.stp.upstreamBuffer[add_actor.peer] = b'' + self.stp.downstreamBuffer[add_actor.peer] = b'' - if delPeer: + if del_actor: # delete the buffer contents associated with the peer - del self.stp.upstreamBuffer[delPeer] - del self.stp.downstreamBuffer[delPeer] + del self.stp.upstreamBuffer[del_actor.peer] + del self.stp.downstreamBuffer[del_actor.peer] # chain this along if self.serviceElement: - self.sap_request(addPeer=addPeer, delPeer=delPeer) - + self.sap_request( + add_actor=add_actor, + del_actor=del_actor, + actor_error=actor_error, error=error, + ) diff --git a/py27/bacpypes/udp.py b/py27/bacpypes/udp.py index e9dfdbba..8c71d8d8 100755 --- a/py27/bacpypes/udp.py +++ b/py27/bacpypes/udp.py @@ -44,19 +44,19 @@ def __init__(self, director, peer): # add a timer self.timeout = director.timeout if self.timeout > 0: - self.timer = FunctionTask(self.IdleTimeout) + self.timer = FunctionTask(self.idle_timeout) self.timer.install_task(_time() + self.timeout) else: self.timer = None # tell the director this is a new actor - self.director.AddActor(self) + self.director.add_actor(self) - def IdleTimeout(self): - if _debug: UDPActor._debug("IdleTimeout") + def idle_timeout(self): + if _debug: UDPActor._debug("idle_timeout") # tell the director this is gone - self.director.RemoveActor(self) + self.director.del_actor(self) def indication(self, pdu): if _debug: UDPActor._debug("indication %r", pdu) @@ -78,6 +78,13 @@ def response(self, pdu): # process this as a response from the director self.director.response(pdu) + def handle_error(self, error=None): + if _debug: UDPActor._debug("handle_error %r", error) + + # pass along to the director + if error is not None: + self.director.actor_error(self, error) + # # UDPPickleActor # @@ -156,52 +163,63 @@ def __init__(self, address, timeout=0, reuse=False, actorClass=UDPActor, sid=Non # start with an empty peer pool self.peers = {} - def AddActor(self, actor): + def add_actor(self, actor): """Add an actor when a new one is connected.""" - if _debug: UDPDirector._debug("AddActor %r", actor) + if _debug: UDPDirector._debug("add_actor %r", actor) self.peers[actor.peer] = actor # tell the ASE there is a new client if self.serviceElement: - self.sap_request(addPeer=actor.peer) + self.sap_request(add_actor=actor) - def RemoveActor(self, actor): + def del_actor(self, actor): """Remove an actor when the socket is closed.""" - if _debug: UDPDirector._debug("RemoveActor %r", actor) + if _debug: UDPDirector._debug("del_actor %r", actor) del self.peers[actor.peer] # tell the ASE the client has gone away if self.serviceElement: - self.sap_request(delPeer=actor.peer) + self.sap_request(del_actor=actor) + + def actor_error(self, actor, error): + if _debug: UDPDirector._debug("actor_error %r %r", actor, error) + + # tell the ASE the actor had an error + if self.serviceElement: + self.sap_request(actor_error=actor, error=error) - def GetActor(self, address): + def get_actor(self, address): return self.peers.get(address, None) def handle_connect(self): - if _debug: deferred(UDPDirector._debug, "handle_connect") + if _debug: UDPDirector._debug("handle_connect") def readable(self): return 1 def handle_read(self): - if _debug: deferred(UDPDirector._debug, "handle_read") + if _debug: UDPDirector._debug("handle_read") try: msg, addr = self.socket.recvfrom(65536) - if _debug: deferred(UDPDirector._debug, " - received %d octets from %s", len(msg), addr) + if _debug: UDPDirector._debug(" - received %d octets from %s", len(msg), addr) # send the PDU up to the client deferred(self._response, PDU(msg, source=addr)) except socket.timeout as err: - deferred(UDPDirector._error, "handle_read socket timeout: %s", err) - except OSError as err: + if _debug: UDPDirector._debug(" - socket timeout: %s", err) + + except socket.error as err: if err.args[0] == 11: pass else: - deferred(UDPDirector._error, "handle_read socket error: %s", err) + if _debug: UDPDirector._debug(" - socket error: %s", err) + + # pass along to a handler + self.handle_error(err) def writable(self): """Return true iff there is a request pending.""" @@ -209,24 +227,37 @@ def writable(self): def handle_write(self): """get a PDU from the queue and send it.""" - if _debug: deferred(UDPDirector._debug, "handle_write") + if _debug: UDPDirector._debug("handle_write") try: pdu = self.request.get() sent = self.socket.sendto(pdu.pduData, pdu.pduDestination) - if _debug: deferred(UDPDirector._debug, " - sent %d octets to %s", sent, pdu.pduDestination) + if _debug: UDPDirector._debug(" - sent %d octets to %s", sent, pdu.pduDestination) - except OSError as err: - deferred(UDPDirector._error, "handle_write socket error: %s", err) + except socket.error as err: + if _debug: UDPDirector._debug(" - socket error: %s", err) + + # get the peer + peer = self.peers.get(pdu.pduDestination, None) + if peer: + # let the actor handle the error + peer.handle_error(err) + else: + # let the director handle the error + self.handle_error(err) def handle_close(self): """Remove this from the monitor when it's closed.""" - if _debug: deferred(UDPDirector._debug, "handle_close") + if _debug: UDPDirector._debug("handle_close") self.close() self.socket = None + def handle_error(self, error=None): + """Handle an error...""" + if _debug: UDPDirector._debug("handle_error %r", error) + def indication(self, pdu): """Client requests are queued for delivery.""" if _debug: UDPDirector._debug("indication %r", pdu) diff --git a/py34/bacpypes/__init__.py b/py34/bacpypes/__init__.py index 4b118603..82684052 100755 --- a/py34/bacpypes/__init__.py +++ b/py34/bacpypes/__init__.py @@ -18,7 +18,7 @@ # Project Metadata # -__version__ = '0.14.2' +__version__ = '0.15.0' __author__ = 'Joel Bender' __email__ = 'joel@carrickbender.com' @@ -29,6 +29,8 @@ from . import comm from . import task from . import singleton +from . import capability +from . import iocb # # Link Layer Modules @@ -67,6 +69,7 @@ from . import app from . import appservice +from . import service # # Analysis diff --git a/py34/bacpypes/app.py b/py34/bacpypes/app.py index 33e4a364..fe974d64 100755 --- a/py34/bacpypes/app.py +++ b/py34/bacpypes/app.py @@ -4,38 +4,34 @@ Application Module """ +import warnings + from .debugging import bacpypes_debugging, DebugContents, ModuleLogger from .comm import ApplicationServiceElement, bind +from .iocb import IOController, SieveQueue -from .pdu import Address, LocalStation, RemoteStation +from .pdu import Address -from .primitivedata import Atomic, Date, Null, ObjectIdentifier, Time, Unsigned -from .constructeddata import Any, Array, ArrayOf +from .primitivedata import ObjectIdentifier +from .capability import Collector from .appservice import StateMachineAccessPoint, ApplicationServiceAccessPoint from .netservice import NetworkServiceAccessPoint, NetworkServiceElement from .bvllservice import BIPSimple, BIPForeign, AnnexJCodec, UDPMultiplexer -from .object import Property, PropertyError, DeviceObject, \ - registered_object_types, register_object_type -from .apdu import ConfirmedRequestPDU, SimpleAckPDU, RejectPDU, RejectReason -from .apdu import IAmRequest, ReadPropertyACK, Error -from .errors import ExecutionError, \ - RejectException, UnrecognizedService, MissingRequiredParameter, \ - ParameterOutOfRange, \ - AbortException +from .apdu import UnconfirmedRequestPDU, ConfirmedRequestPDU, \ + SimpleAckPDU, ComplexAckPDU, ErrorPDU, RejectPDU, AbortPDU, Error + +from .errors import ExecutionError, UnrecognizedService, AbortException, RejectException # for computing protocol services supported from .apdu import confirmed_request_types, unconfirmed_request_types, \ ConfirmedServiceChoice, UnconfirmedServiceChoice from .basetypes import ServicesSupported -from .apdu import \ - AtomicReadFileACK, \ - AtomicReadFileACKAccessMethodChoice, \ - AtomicReadFileACKAccessMethodRecordAccess, \ - AtomicReadFileACKAccessMethodStreamAccess, \ - AtomicWriteFileACK +# basic services +from .service.device import WhoIsIAmServices +from .service.object import ReadWritePropertyServices # some debugging _debug = 0 @@ -149,7 +145,7 @@ def get_device_info(self, key): def update_device_info(self, info): """The application has updated one or more fields in the device information record and the cache needs to be updated to reflect the - changes. If this is a cached version of a persistent record then this + changes. If this is a cached version of a persistent record then this is the opportunity to update the database.""" if _debug: DeviceInfoCache._debug("update_device_info %r", info) @@ -187,146 +183,53 @@ def release_device_info(self, info): if cache_address is not None: del self.cache[cache_address] -# -# CurrentDateProperty -# - -class CurrentDateProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Date, default=None, optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("{0} is unsubscriptable".format(self.identifier)) - - # get the value - now = Date() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# CurrentTimeProperty -# - -class CurrentTimeProperty(Property): - - def __init__(self, identifier): - Property.__init__(self, identifier, Time, default=None, optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - # access an array - if arrayIndex is not None: - raise TypeError("{0} is unsubscriptable".format(self.identifier)) - - # get the value - now = Time() - now.now() - return now.value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# LocalDeviceObject -# - -@bacpypes_debugging -class LocalDeviceObject(DeviceObject): - - properties = \ - [ CurrentTimeProperty('localTime') - , CurrentDateProperty('localDate') - ] - - defaultProperties = \ - { 'maxApduLengthAccepted': 1024 - , 'segmentationSupported': 'segmentedBoth' - , 'maxSegmentsAccepted': 16 - , 'apduSegmentTimeout': 5000 - , 'apduTimeout': 3000 - , 'numberOfApduRetries': 3 - } - - def __init__(self, **kwargs): - if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) - - # fill in default property values not in kwargs - for attr, value in LocalDeviceObject.defaultProperties.items(): - if attr not in kwargs: - kwargs[attr] = value - - # check for registration - if self.__class__ not in registered_object_types.values(): - if 'vendorIdentifier' not in kwargs: - raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") - register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) - - # check for local time - if 'localDate' in kwargs: - raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") - if 'localTime' in kwargs: - raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") - - # check for a minimum value - if kwargs['maxApduLengthAccepted'] < 50: - raise ValueError("invalid max APDU length accepted") - - # dump the updated attributes - if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) - - # proceed as usual - DeviceObject.__init__(self, **kwargs) - - # create a default implementation of an object list for local devices. - # If it is specified in the kwargs, that overrides this default. - if ('objectList' not in kwargs): - self.objectList = ArrayOf(ObjectIdentifier)([self.objectIdentifier]) - - # if the object has a property list and one wasn't provided - # in the kwargs, then it was created by default and the objectList - # property should be included - if ('propertyList' not in kwargs) and self.propertyList: - # make sure it's not already there - if 'objectList' not in self.propertyList: - self.propertyList.append('objectList') - # # Application # @bacpypes_debugging -class Application(ApplicationServiceElement): +class Application(ApplicationServiceElement, Collector): - def __init__(self, localDevice, localAddress, deviceInfoCache=None, aseID=None): + def __init__(self, localDevice=None, localAddress=None, deviceInfoCache=None, aseID=None): if _debug: Application._debug("__init__ %r %r deviceInfoCache=%r aseID=%r", localDevice, localAddress, deviceInfoCache, aseID) ApplicationServiceElement.__init__(self, aseID) + # local objects by ID and name + self.objectName = {} + self.objectIdentifier = {} + # keep track of the local device - self.localDevice = localDevice + if localDevice: + self.localDevice = localDevice - # use the provided cache or make a default one - if deviceInfoCache: - self.deviceInfoCache = deviceInfoCache - else: - self.deviceInfoCache = DeviceInfoCache() + # bind the device object to this application + localDevice._app = self - # bind the device object to this application - localDevice._app = self + # local objects by ID and name + self.objectName[localDevice.objectName] = localDevice + self.objectIdentifier[localDevice.objectIdentifier] = localDevice - # allow the address to be cast to the correct type - if isinstance(localAddress, Address): - self.localAddress = localAddress - else: - self.localAddress = Address(localAddress) + # local address deprecated, but continue to use the old initializer + if localAddress is not None: + warnings.warn( + "local address at the application layer deprecated", + DeprecationWarning, + ) - # local objects by ID and name - self.objectName = {localDevice.objectName:localDevice} - self.objectIdentifier = {localDevice.objectIdentifier:localDevice} + # allow the address to be cast to the correct type + if isinstance(localAddress, Address): + self.localAddress = localAddress + else: + self.localAddress = Address(localAddress) + + # use the provided cache or make a default one + self.deviceInfoCache = deviceInfoCache or DeviceInfoCache() + + # controllers for managing confirmed requests as a client + self.controllers = {} + + # now set up the rest of the capabilities + Collector.__init__(self) def add_object(self, obj): """Add an object to the local collection.""" @@ -354,8 +257,10 @@ def add_object(self, obj): self.objectName[object_name] = obj self.objectIdentifier[object_identifier] = obj - # append the new object's identifier to the device's object list - self.localDevice.objectList.append(object_identifier) + # append the new object's identifier to the local device's object list + # if there is one and it has an object list property + if self.localDevice and self.localDevice.objectList: + self.localDevice.objectList.append(object_identifier) # let the object know which application stack it belongs to obj._app = self @@ -373,8 +278,10 @@ def delete_object(self, obj): del self.objectIdentifier[object_identifier] # remove the object's identifier from the device's object list - indx = self.localDevice.objectList.index(object_identifier) - del self.localDevice.objectList[indx] + # if there is one and it has an object list property + if self.localDevice and self.localDevice.objectList: + indx = self.localDevice.objectList.index(object_identifier) + del self.localDevice.objectList[indx] # make sure the object knows it's detached from an application obj._app = None @@ -417,6 +324,16 @@ def get_services_supported(self): #----- + def request(self, apdu): + if _debug: Application._debug("request %r", apdu) + + # double check the input is the right kind of APDU + if not isinstance(apdu, (UnconfirmedRequestPDU, ConfirmedRequestPDU)): + raise TypeError("APDU expected") + + # continue + super(Application, self).request(apdu) + def indication(self, apdu): if _debug: Application._debug("indication %r", apdu) @@ -456,345 +373,99 @@ def indication(self, apdu): resp = Error(errorClass='device', errorCode='operationalProblem', context=apdu) self.response(resp) - def do_WhoIsRequest(self, apdu): - """Respond to a Who-Is request.""" - if _debug: Application._debug("do_WhoIsRequest %r", apdu) - - # extract the parameters - low_limit = apdu.deviceInstanceRangeLowLimit - high_limit = apdu.deviceInstanceRangeHighLimit - - # check for consistent parameters - if (low_limit is not None): - if (high_limit is None): - raise MissingRequiredParameter("deviceInstanceRangeHighLimit required") - if (low_limit < 0) or (low_limit > 4194303): - raise ParameterOutOfRange("deviceInstanceRangeLowLimit out of range") - if (high_limit is not None): - if (low_limit is None): - raise MissingRequiredParameter("deviceInstanceRangeLowLimit required") - if (high_limit < 0) or (high_limit > 4194303): - raise ParameterOutOfRange("deviceInstanceRangeHighLimit out of range") - - # see we should respond - if (low_limit is not None): - if (self.localDevice.objectIdentifier[1] < low_limit): - return - if (high_limit is not None): - if (self.localDevice.objectIdentifier[1] > high_limit): - return +# +# ApplicationIOController +# - # create a I-Am "response" back to the source - iAm = IAmRequest() - iAm.pduDestination = apdu.pduSource - iAm.iAmDeviceIdentifier = self.localDevice.objectIdentifier - iAm.maxAPDULengthAccepted = self.localDevice.maxApduLengthAccepted - iAm.segmentationSupported = self.localDevice.segmentationSupported - iAm.vendorID = self.localDevice.vendorIdentifier - if _debug: Application._debug(" - iAm: %r", iAm) +@bacpypes_debugging +class ApplicationIOController(IOController, Application): - # away it goes - self.request(iAm) + def __init__(self, *args, **kwargs): + if _debug: ApplicationIOController._debug("__init__") + IOController.__init__(self) + Application.__init__(self, *args, **kwargs) - def do_IAmRequest(self, apdu): - """Respond to an I-Am request.""" - if _debug: Application._debug("do_IAmRequest %r", apdu) + # queues for each address + self.queue_by_address = {} - def do_ReadPropertyRequest(self, apdu): - """Return the value of some property of one of our objects.""" - if _debug: Application._debug("do_ReadPropertyRequest %r", apdu) + def process_io(self, iocb): + if _debug: ApplicationIOController._debug("process_io %r", iocb) - # extract the object identifier - objId = apdu.objectIdentifier + # get the destination address from the pdu + destination_address = iocb.args[0].pduDestination + if _debug: ApplicationIOController._debug(" - destination_address: %r", destination_address) - # check for wildcard - if (objId == ('device', 4194303)): - if _debug: Application._debug(" - wildcard device identifier") - objId = self.localDevice.objectIdentifier + # look up the queue + queue = self.queue_by_address.get(destination_address, None) + if not queue: + queue = SieveQueue(self.request, destination_address) + self.queue_by_address[destination_address] = queue + if _debug: ApplicationIOController._debug(" - queue: %r", queue) - # get the object - obj = self.get_object_id(objId) - if _debug: Application._debug(" - object: %r", obj) + # ask the queue to process the request + queue.request_io(iocb) - if not obj: - resp = Error(errorClass='object', errorCode='unknownObject', context=apdu) - else: - try: - # get the datatype - datatype = obj.get_datatype(apdu.propertyIdentifier) - if _debug: Application._debug(" - datatype: %r", datatype) - - # get the value - value = obj.ReadProperty(apdu.propertyIdentifier, apdu.propertyArrayIndex) - if _debug: Application._debug(" - value: %r", value) - if value is None: - raise PropertyError(apdu.propertyIdentifier) - - # change atomic values into something encodeable - if issubclass(datatype, Atomic): - value = datatype(value) - elif issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): - if apdu.propertyArrayIndex == 0: - value = Unsigned(value) - elif issubclass(datatype.subtype, Atomic): - value = datatype.subtype(value) - elif not isinstance(value, datatype.subtype): - raise TypeError("invalid result datatype, expecting {0} and got {1}" \ - .format(datatype.subtype.__name__, type(value).__name__)) - elif not isinstance(value, datatype): - raise TypeError("invalid result datatype, expecting {0} and got {1}" \ - .format(datatype.__name__, type(value).__name__)) - if _debug: Application._debug(" - encodeable value: %r", value) - - # this is a ReadProperty ack - resp = ReadPropertyACK(context=apdu) - resp.objectIdentifier = objId - resp.propertyIdentifier = apdu.propertyIdentifier - resp.propertyArrayIndex = apdu.propertyArrayIndex - - # save the result in the property value - resp.propertyValue = Any() - resp.propertyValue.cast_in(value) - - except PropertyError: - resp = Error(errorClass='object', errorCode='unknownProperty', context=apdu) - if _debug: Application._debug(" - resp: %r", resp) - - # return the result - self.response(resp) - - def do_WritePropertyRequest(self, apdu): - """Change the value of some property of one of our objects.""" - if _debug: Application._debug("do_WritePropertyRequest %r", apdu) - - # get the object - obj = self.get_object_id(apdu.objectIdentifier) - if _debug: Application._debug(" - object: %r", obj) - - if not obj: - resp = Error(errorClass='object', errorCode='unknownObject', context=apdu) - else: - try: - # check if the property exists - if obj.ReadProperty(apdu.propertyIdentifier, apdu.propertyArrayIndex) is None: - raise PropertyError(apdu.propertyIdentifier) - - # get the datatype, special case for null - if apdu.propertyValue.is_application_class_null(): - datatype = Null - else: - datatype = obj.get_datatype(apdu.propertyIdentifier) - if _debug: Application._debug(" - datatype: %r", datatype) - - # special case for array parts, others are managed by cast_out - if issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): - if apdu.propertyArrayIndex == 0: - value = apdu.propertyValue.cast_out(Unsigned) - else: - value = apdu.propertyValue.cast_out(datatype.subtype) - else: - value = apdu.propertyValue.cast_out(datatype) - if _debug: Application._debug(" - value: %r", value) - - # change the value - value = obj.WriteProperty(apdu.propertyIdentifier, value, apdu.propertyArrayIndex, apdu.priority) - - # success - resp = SimpleAckPDU(context=apdu) - - except PropertyError: - resp = Error(errorClass='object', errorCode='unknownProperty', context=apdu) - if _debug: Application._debug(" - resp: %r", resp) - - # return the result - self.response(resp) - - def do_AtomicReadFileRequest(self, apdu): - """Return one of our records.""" - if _debug: Application._debug("do_AtomicReadFileRequest %r", apdu) - - if (apdu.fileIdentifier[0] != 'file'): - resp = Error(errorClass='services', errorCode='inconsistentObjectType', context=apdu) - if _debug: Application._debug(" - error resp: %r", resp) - self.response(resp) - return + def _app_complete(self, address, apdu): + if _debug: ApplicationIOController._debug("_app_complete %r %r", address, apdu) - # get the object - obj = self.get_object_id(apdu.fileIdentifier) - if _debug: Application._debug(" - object: %r", obj) - - if not obj: - resp = Error(errorClass='object', errorCode='unknownObject', context=apdu) - elif apdu.accessMethod.recordAccess: - # check against the object - if obj.fileAccessMethod != 'recordAccess': - resp = Error(errorClass='services', - errorCode='invalidFileAccessMethod', - context=apdu - ) - ### verify start is valid - double check this (empty files?) - elif (apdu.accessMethod.recordAccess.fileStartRecord < 0) or \ - (apdu.accessMethod.recordAccess.fileStartRecord >= len(obj)): - resp = Error(errorClass='services', - errorCode='invalidFileStartPosition', - context=apdu - ) - else: - # pass along to the object - end_of_file, record_data = obj.ReadFile( - apdu.accessMethod.recordAccess.fileStartRecord, - apdu.accessMethod.recordAccess.requestedRecordCount, - ) - if _debug: Application._debug(" - record_data: %r", record_data) - - # this is an ack - resp = AtomicReadFileACK(context=apdu, - endOfFile=end_of_file, - accessMethod=AtomicReadFileACKAccessMethodChoice( - recordAccess=AtomicReadFileACKAccessMethodRecordAccess( - fileStartRecord=apdu.accessMethod.recordAccess.fileStartRecord, - returnedRecordCount=len(record_data), - fileRecordData=record_data, - ), - ), - ) - - elif apdu.accessMethod.streamAccess: - # check against the object - if obj.fileAccessMethod != 'streamAccess': - resp = Error(errorClass='services', - errorCode='invalidFileAccessMethod', - context=apdu - ) - ### verify start is valid - double check this (empty files?) - elif (apdu.accessMethod.streamAccess.fileStartPosition < 0) or \ - (apdu.accessMethod.streamAccess.fileStartPosition >= len(obj)): - resp = Error(errorClass='services', - errorCode='invalidFileStartPosition', - context=apdu - ) - else: - # pass along to the object - end_of_file, record_data = obj.ReadFile( - apdu.accessMethod.streamAccess.fileStartPosition, - apdu.accessMethod.streamAccess.requestedOctetCount, - ) - if _debug: Application._debug(" - record_data: %r", record_data) - - # this is an ack - resp = AtomicReadFileACK(context=apdu, - endOfFile=end_of_file, - accessMethod=AtomicReadFileACKAccessMethodChoice( - streamAccess=AtomicReadFileACKAccessMethodStreamAccess( - fileStartPosition=apdu.accessMethod.streamAccess.fileStartPosition, - fileData=record_data, - ), - ), - ) - - if _debug: Application._debug(" - resp: %r", resp) - - # return the result - self.response(resp) - - def do_AtomicWriteFileRequest(self, apdu): - """Return one of our records.""" - if _debug: Application._debug("do_AtomicWriteFileRequest %r", apdu) - - if (apdu.fileIdentifier[0] != 'file'): - resp = Error(errorClass='services', errorCode='inconsistentObjectType', context=apdu) - if _debug: Application._debug(" - error resp: %r", resp) - self.response(resp) + # look up the queue + queue = self.queue_by_address.get(address, None) + if not queue: + ApplicationIOController._debug("no queue for %r" % (address,)) return + if _debug: ApplicationIOController._debug(" - queue: %r", queue) - # get the object - obj = self.get_object_id(apdu.fileIdentifier) - if _debug: Application._debug(" - object: %r", obj) - - if not obj: - resp = Error(errorClass='object', errorCode='unknownObject', context=apdu) - elif apdu.accessMethod.recordAccess: - # check against the object - if obj.fileAccessMethod != 'recordAccess': - resp = Error(errorClass='services', - errorCode='invalidFileAccessMethod', - context=apdu - ) - if _debug: Application._debug(" - error resp: %r", resp) - self.response(resp) - return - - # check for read-only - if obj.readOnly: - resp = Error(errorClass='services', - errorCode='fileAccessDenied', - context=apdu - ) - if _debug: Application._debug(" - error resp: %r", resp) - self.response(resp) - return - - # pass along to the object - start_record = obj.WriteFile( - apdu.accessMethod.recordAccess.fileStartRecord, - apdu.accessMethod.recordAccess.recordCount, - apdu.accessMethod.recordAccess.fileRecordData, - ) - if _debug: Application._debug(" - start_record: %r", start_record) + # make sure it has an active iocb + if not queue.active_iocb: + ApplicationIOController._debug("no active request for %r" % (address,)) + return - # this is an ack - resp = AtomicWriteFileACK(context=apdu, - fileStartRecord=start_record, - ) + # this request is complete + if isinstance(apdu, (None.__class__, SimpleAckPDU, ComplexAckPDU)): + queue.complete_io(queue.active_iocb, apdu) + elif isinstance(apdu, (ErrorPDU, RejectPDU, AbortPDU)): + queue.abort_io(queue.active_iocb, apdu) + else: + raise RuntimeError("unrecognized APDU type") + if _debug: Application._debug(" - controller finished") - elif apdu.accessMethod.streamAccess: - # check against the object - if obj.fileAccessMethod != 'streamAccess': - resp = Error(errorClass='services', - errorCode='invalidFileAccessMethod', - context=apdu - ) - if _debug: Application._debug(" - error resp: %r", resp) - self.response(resp) - return + # if the queue is empty and idle, forget about the controller + if not queue.ioQueue.queue and not queue.active_iocb: + if _debug: ApplicationIOController._debug(" - queue is empty") + del self.queue_by_address[address] - # check for read-only - if obj.readOnly: - resp = Error(errorClass='services', - errorCode='fileAccessDenied', - context=apdu - ) - if _debug: Application._debug(" - error resp: %r", resp) - self.response(resp) - return + def request(self, apdu): + if _debug: ApplicationIOController._debug("request %r", apdu) - # pass along to the object - start_position = obj.WriteFile( - apdu.accessMethod.streamAccess.fileStartPosition, - apdu.accessMethod.streamAccess.fileData, - ) - if _debug: Application._debug(" - start_position: %r", start_position) + # send it downstream + super(ApplicationIOController, self).request(apdu) - # this is an ack - resp = AtomicWriteFileACK(context=apdu, - fileStartPosition=start_position, - ) + # if this was an unconfirmed request, it's complete, no message + if isinstance(apdu, UnconfirmedRequestPDU): + self._app_complete(apdu.pduDestination, None) - if _debug: Application._debug(" - resp: %r", resp) + def confirmation(self, apdu): + if _debug: ApplicationIOController._debug("confirmation %r", apdu) - # return the result - self.response(resp) + # this is an ack, error, reject or abort + self._app_complete(apdu.pduSource, apdu) # # BIPSimpleApplication # @bacpypes_debugging -class BIPSimpleApplication(Application): +class BIPSimpleApplication(ApplicationIOController, WhoIsIAmServices, ReadWritePropertyServices): def __init__(self, localDevice, localAddress, deviceInfoCache=None, aseID=None): if _debug: BIPSimpleApplication._debug("__init__ %r %r deviceInfoCache=%r aseID=%r", localDevice, localAddress, deviceInfoCache, aseID) - Application.__init__(self, localDevice, localAddress, deviceInfoCache, aseID) + ApplicationIOController.__init__(self, localDevice, deviceInfoCache, aseID=aseID) + + # local address might be useful for subclasses + if isinstance(localAddress, Address): + self.localAddress = localAddress + else: + self.localAddress = Address(localAddress) # include a application decoder self.asap = ApplicationServiceAccessPoint() @@ -834,11 +505,17 @@ def __init__(self, localDevice, localAddress, deviceInfoCache=None, aseID=None): # @bacpypes_debugging -class BIPForeignApplication(Application): +class BIPForeignApplication(ApplicationIOController, WhoIsIAmServices, ReadWritePropertyServices): def __init__(self, localDevice, localAddress, bbmdAddress, bbmdTTL, aseID=None): if _debug: BIPForeignApplication._debug("__init__ %r %r %r %r aseID=%r", localDevice, localAddress, bbmdAddress, bbmdTTL, aseID) - Application.__init__(self, localDevice, localAddress, aseID) + ApplicationIOController.__init__(self, localDevice, aseID=aseID) + + # local address might be useful for subclasses + if isinstance(localAddress, Address): + self.localAddress = localAddress + else: + self.localAddress = Address(localAddress) # include a application decoder self.asap = ApplicationServiceAccessPoint() @@ -903,4 +580,3 @@ def __init__(self, localAddress, eID=None): # bind the NSAP to the stack, no network number self.nsap.bind(self.bip) - diff --git a/py34/bacpypes/appservice.py b/py34/bacpypes/appservice.py index 918f7f22..f11edd17 100755 --- a/py34/bacpypes/appservice.py +++ b/py34/bacpypes/appservice.py @@ -235,7 +235,7 @@ def FillWindow(self, seqNum): class ClientSSM(SSM): def __init__(self, sap, remoteDevice): - if _debug: ClientSSM._debug("__init__ %s %r %r", sap, remoteDevice) + if _debug: ClientSSM._debug("__init__ %s %r", sap, remoteDevice) SSM.__init__(self, sap, remoteDevice) # initialize the retry count @@ -644,7 +644,7 @@ def segmented_confirmation_timeout(self): class ServerSSM(SSM): def __init__(self, sap, remoteDevice): - if _debug: ServerSSM._debug("__init__ %s %r %r", sap, remoteDevice) + if _debug: ServerSSM._debug("__init__ %s %r", sap, remoteDevice) SSM.__init__(self, sap, remoteDevice) def set_state(self, newState, timer=0): @@ -1061,8 +1061,7 @@ def __init__(self, localDevice=None, deviceInfoCache=None, sap=None, cid=None): Client.__init__(self, cid) ServiceAccessPoint.__init__(self, sap) - # save a reference to the local device object and the cache - self.localDevice = localDevice + # save a reference to the device information cache self.deviceInfoCache = deviceInfoCache # client settings diff --git a/py34/bacpypes/capability.py b/py34/bacpypes/capability.py new file mode 100644 index 00000000..2aa18537 --- /dev/null +++ b/py34/bacpypes/capability.py @@ -0,0 +1,151 @@ +#!/usr/bin/python + +""" +Capability +""" + +from .debugging import bacpypes_debugging, ModuleLogger + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# Capability +# + +@bacpypes_debugging +class Capability(object): + + _zindex = 99 + + def __init__(self): + if _debug: Capability._debug("__init__") + +# +# Collector +# + +@bacpypes_debugging +class Collector(object): + + def __init__(self): + if _debug: Collector._debug("__init__ (%r %r)", self.__class__, self.__class__.__bases__) + + # gather the capbilities + self.capabilities = self._search_capability(self.__class__) + + # give them a chance to init + for cls in self.capabilities: + if hasattr(cls, '__init__') and cls is not Collector: + if _debug: Collector._debug(" - calling %r.__init__", cls) + cls.__init__(self) + + def _search_capability(self, base): + """Given a class, return a list of all of the derived classes that + are themselves derived from Capability.""" + if _debug: Collector._debug("_search_capability %r", base) + + rslt = [] + for cls in base.__bases__: + if issubclass(cls, Collector): + map( rslt.append, self._search_capability(cls)) + elif issubclass(cls, Capability): + rslt.append(cls) + if _debug: Collector._debug(" - rslt: %r", rslt) + + return rslt + + def capability_functions(self, fn): + """This generator yields functions that match the + requested capability sorted by z-index.""" + if _debug: Collector._debug("capability_functions %r", fn) + + # build a list of functions to call + fns = [] + for cls in self.capabilities: + xfn = getattr(cls, fn, None) + if _debug: Collector._debug(" - cls, xfn: %r, %r", cls, xfn) + if xfn: + fns.append( (getattr(cls, '_zindex', None), xfn) ) + + # sort them by z-index + fns.sort(key=lambda v: v[0]) + if _debug: Collector._debug(" - fns: %r", fns) + + # now yield them in order + for xindx, xfn in fns: + if _debug: Collector._debug(" - yield xfn: %r", xfn) + yield xfn + + def add_capability(self, cls): + """Add a capability to this object.""" + if _debug: Collector._debug("add_capability %r", cls) + + # the new type has everything the current one has plus this new one + bases = (self.__class__, cls) + if _debug: Collector._debug(" - bases: %r", bases) + + # save this additional class + self.capabilities.append(cls) + + # morph into a new type + newtype = type(self.__class__.__name__ + '+' + cls.__name__, bases, {}) + self.__class__ = newtype + + # allow the new type to init + if hasattr(cls, '__init__'): + if _debug: Collector._debug(" - calling %r.__init__", cls) + cls.__init__(self) + +# +# compose_capability +# + +@bacpypes_debugging +def compose_capability(base, *classes): + """Create a new class starting with the base and adding capabilities.""" + if _debug: compose_capability._debug("compose_capability %r %r", base, classes) + + # make sure the base is a Collector + if not issubclass(base, Collector): + raise TypeError("base must be a subclass of Collector") + + # make sure you only add capabilities + for cls in classes: + if not issubclass(cls, Capability): + raise TypeError("%s is not a Capability subclass" % (cls,)) + + # start with everything the base has and add the new ones + bases = (base,) + classes + + # build a new name + name = base.__name__ + for cls in classes: + name += '+' + cls.__name__ + + # return a new type + return type(name, bases, {}) + +# +# add_capability +# + +@bacpypes_debugging +def add_capability(base, *classes): + """Add capabilites to an existing base, all objects get the additional + functionality, but don't get inited. Use with great care!""" + if _debug: add_capability._debug("add_capability %r %r", base, classes) + + # start out with a collector + if not issubclass(base, Collector): + raise TypeError("base must be a subclass of Collector") + + # make sure you only add capabilities + for cls in classes: + if not issubclass(cls, Capability): + raise TypeError("%s is not a Capability subclass" % (cls,)) + + base.__bases__ += classes + for cls in classes: + base.__name__ += '+' + cls.__name__ diff --git a/py34/bacpypes/comm.py b/py34/bacpypes/comm.py index 0743bc0b..82a10127 100755 --- a/py34/bacpypes/comm.py +++ b/py34/bacpypes/comm.py @@ -108,12 +108,12 @@ def __init__(self, data=None, *args, **kwargs): super(PDUData, self).__init__(*args, **kwargs) # function acts like a copy constructor - if isinstance(data, PDUData) or isinstance(data, PDU): - self.pduData = _copy(data.pduData) - elif data is None: + if data is None: self.pduData = bytearray() elif isinstance(data, (bytes, bytearray)): self.pduData = bytearray(data) + elif isinstance(data, PDUData) or isinstance(data, PDU): + self.pduData = _copy(data.pduData) else: raise TypeError("bytes or bytearray expected") @@ -207,7 +207,7 @@ def dict_contents(self, use_dict=None, as_class=dict): @bacpypes_debugging class PDU(PCI, PDUData): - def __init__(self, data='', **kwargs): + def __init__(self, data=None, **kwargs): if _debug: PDU._debug("__init__ %r %r", data, kwargs) # pick up some optional kwargs @@ -236,7 +236,7 @@ def __str__(self): def dict_contents(self, use_dict=None, as_class=dict): """Return the contents of an object as a dict.""" - if _debug: PDUData._debug("dict_contents use_dict=%r as_class=%r", use_dict, as_class) + if _debug: PDU._debug("dict_contents use_dict=%r as_class=%r", use_dict, as_class) # make/extend the dictionary of content if use_dict is None: diff --git a/py34/bacpypes/console.py b/py34/bacpypes/console.py index 944b7e92..70584100 100755 --- a/py34/bacpypes/console.py +++ b/py34/bacpypes/console.py @@ -50,14 +50,23 @@ def writable(self): def handle_read(self): if _debug: deferred(ConsoleClient._debug, "handle_read") - data = sys.stdin.read() + + # read from stdin and encode it + data = sys.stdin.read().encode('utf-8') if _debug: deferred(ConsoleClient._debug, " - data: %r", data) - deferred(self.request, PDU(data.encode('utf_8'))) + + # make a PDU and send it downstream + deferred(self.request, PDU(data)) def confirmation(self, pdu): if _debug: deferred(ConsoleClient._debug, "confirmation %r", pdu) try: - sys.stdout.write(pdu.pduData.decode('utf_8')) + # decode the data + data = pdu.pduData.decode('utf-8') + if _debug: deferred(ConsoleClient._debug, " - data: %r", data) + + # send it out + sys.stdout.write(data) except Exception as err: ConsoleClient._exception("Confirmation sys.stdout.write exception: %r", err) @@ -81,13 +90,22 @@ def writable(self): def handle_read(self): if _debug: deferred(ConsoleServer._debug, "handle_read") - data = sys.stdin.read() + + # read from stdin and encode it + data = sys.stdin.read().encode('utf-8') if _debug: deferred(ConsoleServer._debug, " - data: %r", data) - deferred(self.response, PDU(data.encode('utf_8'))) + + # make a PDU and send it upstream + deferred(self.response, PDU(data)) def indication(self, pdu): - if _debug: deferred(ConsoleServer._debug, "Indication %r", pdu) + if _debug: deferred(ConsoleServer._debug, "indication %r", pdu) try: - sys.stdout.write(pdu.pduData.decode('utf_8')) + # decode the data + data = pdu.pduData.decode('utf-8') + if _debug: deferred(ConsoleServer._debug, " - data: %r", data) + + # send it out + sys.stdout.write(data) except Exception as err: - ConsoleServer._exception("Indication sys.stdout.write exception: %r", err) + ConsoleServer._exception("indication sys.stdout.write exception: %r", err) diff --git a/py34/bacpypes/constructeddata.py b/py34/bacpypes/constructeddata.py index e5239155..e9c80bea 100755 --- a/py34/bacpypes/constructeddata.py +++ b/py34/bacpypes/constructeddata.py @@ -292,7 +292,7 @@ def debug_contents(self, indent=1, file=sys.stdout, _ids=None): if element.optional and value is None: continue if not element.optional and value is None: - file.write("%s%s is a required element of %s\n" % (" " * indent, element.name, self.__class__.__name__)) + file.write("%s%s is a missing required element of %s\n" % (" " * indent, element.name, self.__class__.__name__)) continue if element.klass in _sequence_of_classes: diff --git a/py34/bacpypes/core.py b/py34/bacpypes/core.py index 048d9a86..00b4a16e 100755 --- a/py34/bacpypes/core.py +++ b/py34/bacpypes/core.py @@ -79,7 +79,7 @@ def run(spin=SPIN): # call the functions for fn, args, kwargs in fnlist: - # if _debug: run._debug(" - call: %r %r %r", fn, args, kwargs) +# if _debug: run._debug(" - call: %r %r %r", fn, args, kwargs) fn( *args, **kwargs) # done with this list @@ -210,13 +210,22 @@ def print_stack(sig, frame): # deferred # +@bacpypes_debugging def deferred(fn, *args, **kwargs): - # _log.debug("deferred %r %r %r", fn, args, kwargs) - global deferredFns +# if _debug: +# deferred._debug("deferred %r %r %r", fn, args, kwargs) +# for filename, lineno, _, _ in traceback.extract_stack()[-6:-1]: +# deferred._debug(" %s:%s" % (filename.split('/')[-1], lineno)) + global deferredFns, taskManager # append it to the list deferredFns.append((fn, args, kwargs)) + # trigger the task manager event + if taskManager and taskManager.trigger: +# if _debug: deferred._debug(" - trigger") + taskManager.trigger.set() + # # enable_sleeping # diff --git a/py34/bacpypes/errors.py b/py34/bacpypes/errors.py index 7c1dcba5..bb25871b 100755 --- a/py34/bacpypes/errors.py +++ b/py34/bacpypes/errors.py @@ -100,7 +100,7 @@ class InconsistentParameters(RejectException): conditional service argument that should not be present. This condition could also elicit a Reject PDU with a Reject Reason of INVALID_TAG. """ - + rejectReason = 'inconsistentParameters' diff --git a/py34/bacpypes/iocb.py b/py34/bacpypes/iocb.py new file mode 100644 index 00000000..49e8db6d --- /dev/null +++ b/py34/bacpypes/iocb.py @@ -0,0 +1,1002 @@ +#!/usr/bin/python + +""" +IOCB Module +""" + +import sys +import logging +from time import time as _time + +import threading +from bisect import bisect_left + +from .debugging import bacpypes_debugging, ModuleLogger, DebugContents + +from .core import deferred +from .task import FunctionTask +from .comm import Client + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) +_statelog = logging.getLogger(__name__ + "._statelog") + +# globals +local_controllers = {} + +# +# IOCB States +# + +IDLE = 0 # has not been submitted +PENDING = 1 # queued, waiting for processing +ACTIVE = 2 # being processed +COMPLETED = 3 # finished +ABORTED = 4 # finished in a bad way + +_stateNames = { + 0: 'IDLE', + 1: 'PENDING', + 2: 'ACTIVE', + 3: 'COMPLETED', + 4: 'ABORTED', + } + +# +# IOQController States +# + +CTRL_IDLE = 0 # nothing happening +CTRL_ACTIVE = 1 # working on an iocb +CTRL_WAITING = 1 # waiting between iocb requests (throttled) + +_ctrlStateNames = { + 0: 'IDLE', + 1: 'ACTIVE', + 2: 'WAITING', + } + +# special abort error +TimeoutError = RuntimeError("timeout") + +# current time formatting (short version) +_strftime = lambda: "%011.6f" % (_time() % 3600,) + +# +# IOCB - Input Output Control Block +# + +_identNext = 1 +_identLock = threading.Lock() + +@bacpypes_debugging +class IOCB(DebugContents): + + _debugContents = \ + ( 'args', 'kwargs' + , 'ioState', 'ioResponse-', 'ioError' + , 'ioController', 'ioServerRef', 'ioControllerRef', 'ioClientID', 'ioClientAddr' + , 'ioComplete', 'ioCallback+', 'ioQueue', 'ioPriority', 'ioTimeout' + ) + + def __init__(self, *args, **kwargs): + global _identNext + + # lock the identity sequence number + _identLock.acquire() + + # generate a unique identity for this block + ioID = _identNext + _identNext += 1 + + # release the lock + _identLock.release() + + # debugging postponed until ID acquired + if _debug: IOCB._debug("__init__(%d) %r %r", ioID, args, kwargs) + + # save the ID + self.ioID = ioID + + # save the request parameters + self.args = args + self.kwargs = kwargs + + # start with an idle request + self.ioState = IDLE + self.ioResponse = None + self.ioError = None + + # blocks are bound to a controller + self.ioController = None + + # each block gets a completion event + self.ioComplete = threading.Event() + self.ioComplete.clear() + + # applications can set a callback functions + self.ioCallback = [] + + # request is not currently queued + self.ioQueue = None + + # extract the priority if it was given + self.ioPriority = kwargs.get('_priority', 0) + if '_priority' in kwargs: + if _debug: IOCB._debug(" - ioPriority: %r", self.ioPriority) + del kwargs['_priority'] + + # request has no timeout + self.ioTimeout = None + + def add_callback(self, fn, *args, **kwargs): + """Pass a function to be called when IO is complete.""" + if _debug: IOCB._debug("add_callback(%d) %r %r %r", self.ioID, fn, args, kwargs) + + # store it + self.ioCallback.append((fn, args, kwargs)) + + # already complete? + if self.ioComplete.isSet(): + self.trigger() + + def wait(self, *args): + """Wait for the completion event to be set.""" + if _debug: IOCB._debug("wait(%d) %r", self.ioID, args) + + # waiting from a non-daemon thread could be trouble + self.ioComplete.wait(*args) + + def trigger(self): + """Set the completion event and make the callback(s).""" + if _debug: IOCB._debug("trigger(%d)", self.ioID) + + # if it's queued, remove it from its queue + if self.ioQueue: + if _debug: IOCB._debug(" - dequeue") + self.ioQueue.Remove(self) + + # if there's a timer, cancel it + if self.ioTimeout: + if _debug: IOCB._debug(" - cancel timeout") + self.ioTimeout.SuspendTask() + + # set the completion event + self.ioComplete.set() + + # make the callback(s) + for fn, args, kwargs in self.ioCallback: + if _debug: IOCB._debug(" - callback fn: %r %r %r", fn, args, kwargs) + fn(self, *args, **kwargs) + + def complete(self, msg): + """Called to complete a transaction, usually when ProcessIO has + shipped the IOCB off to some other thread or function.""" + if _debug: IOCB._debug("complete(%d) %r", self.ioID, msg) + + if self.ioController: + # pass to controller + self.ioController.complete_io(self, msg) + else: + # just fill in the data + self.ioState = COMPLETED + self.ioResponse = msg + self.trigger() + + def abort(self, err): + """Called by a client to abort a transaction.""" + if _debug: IOCB._debug("abort(%d) %r", self.ioID, err) + + if self.ioController: + # pass to controller + self.ioController.abort_io(self, err) + elif self.ioState < COMPLETED: + # just fill in the data + self.ioState = ABORTED + self.ioError = err + self.trigger() + + def set_timeout(self, delay, err=TimeoutError): + """Called to set a transaction timer.""" + if _debug: IOCB._debug("set_timeout(%d) %r err=%r", self.ioID, delay, err) + + # if one has already been created, cancel it + if self.ioTimeout: + self.ioTimeout.suspend_task() + else: + self.ioTimeout = FunctionTask(self.Abort, err) + + # (re)schedule it + self.ioTimeout.install_task(_time() + delay) + + def __repr__(self): + xid = id(self) + if (xid < 0): xid += (1 << 32) + + sname = self.__module__ + '.' + self.__class__.__name__ + desc = "(%d)" % (self.ioID) + + return '<' + sname + desc + ' instance at 0x%08x' % (xid,) + '>' + +# +# IOChainMixIn +# + +@bacpypes_debugging +class IOChainMixIn(DebugContents): + + _debugContents = ( 'ioChain++', ) + + def __init__(self, iocb): + if _debug: IOChainMixIn._debug("__init__ %r", iocb) + + # save a refence back to the iocb + self.ioChain = iocb + + # set the callback to follow the chain + self.add_callback(self.chain_callback) + + # if we're not chained, there's no notification to do + if not self.ioChain: + return + + # this object becomes its controller + iocb.ioController = self + + # consider the parent active + iocb.ioState = ACTIVE + + try: + if _debug: IOChainMixIn._debug(" - encoding") + + # let the derived class set the args and kwargs + self.encode() + + if _debug: IOChainMixIn._debug(" - encode complete") + except: + # extract the error and abort the request + err = sys.exc_info()[1] + if _debug: IOChainMixIn._exception(" - encoding exception: %r", err) + + iocb.abort(err) + + def chain_callback(self, iocb): + """Callback when this iocb completes.""" + if _debug: IOChainMixIn._debug("chain_callback %r", iocb) + + # if we're not chained, there's no notification to do + if not self.ioChain: + return + + # refer to the chained iocb + iocb = self.ioChain + + try: + if _debug: IOChainMixIn._debug(" - decoding") + + # let the derived class transform the data + self.decode() + + if _debug: IOChainMixIn._debug(" - decode complete") + except: + # extract the error and abort + err = sys.exc_info()[1] + if _debug: IOChainMixIn._exception(" - decoding exception: %r", err) + + iocb.ioState = ABORTED + iocb.ioError = err + + # break the references + self.ioChain = None + iocb.ioController = None + + # notify the client + iocb.trigger() + + def abort_io(self, iocb, err): + """Forward the abort downstream.""" + if _debug: IOChainMixIn._debug("abort_io %r %r", iocb, err) + + # make sure we're being notified of an abort request from + # the iocb we are chained from + if iocb is not self.ioChain: + raise RuntimeError("broken chain") + + # call my own Abort(), which may forward it to a controller or + # be overridden by IOGroup + self.abort(err) + + def encode(self): + """Hook to transform the request, called when this IOCB is + chained.""" + if _debug: IOChainMixIn._debug("encode") + + # by default do nothing, the arguments have already been supplied + + def decode(self): + """Hook to transform the response, called when this IOCB is + completed.""" + if _debug: IOChainMixIn._debug("decode") + + # refer to the chained iocb + iocb = self.ioChain + + # if this has completed successfully, pass it up + if self.ioState == COMPLETED: + if _debug: IOChainMixIn._debug(" - completed: %r", self.ioResponse) + + # change the state and transform the content + iocb.ioState = COMPLETED + iocb.ioResponse = self.ioResponse + + # if this aborted, pass that up too + elif self.ioState == ABORTED: + if _debug: IOChainMixIn._debug(" - aborted: %r", self.ioError) + + # change the state + iocb.ioState = ABORTED + iocb.ioError = self.ioError + + else: + raise RuntimeError("invalid state: %d" % (self.ioState,)) + +# +# IOChain +# + +@bacpypes_debugging +class IOChain(IOCB, IOChainMixIn): + + def __init__(self, chain, *args, **kwargs): + """Initialize a chained control block.""" + if _debug: IOChain._debug("__init__ %r %r %r", chain, args, kwargs) + + # initialize IOCB part to pick up the ioID + IOCB.__init__(self, *args, **kwargs) + IOChainMixIn.__init__(self, chain) + +# +# IOGroup +# + +@bacpypes_debugging +class IOGroup(IOCB, DebugContents): + + _debugContents = ('ioMembers',) + + def __init__(self): + """Initialize a group.""" + if _debug: IOGroup._debug("__init__") + IOCB.__init__(self) + + # start with an empty list of members + self.ioMembers = [] + + # start out being done. When an IOCB is added to the + # group that is not already completed, this state will + # change to PENDING. + self.ioState = COMPLETED + self.ioComplete.set() + + def add(self, iocb): + """Add an IOCB to the group, you can also add other groups.""" + if _debug: IOGroup._debug("add %r", iocb) + + # add this to our members + self.ioMembers.append(iocb) + + # assume all of our members have not completed yet + self.ioState = PENDING + self.ioComplete.clear() + + # when this completes, call back to the group. If this + # has already completed, it will trigger + iocb.add_callback(self.group_callback) + + def group_callback(self, iocb): + """Callback when a child iocb completes.""" + if _debug: IOGroup._debug("group_callback %r", iocb) + + # check all the members + for iocb in self.ioMembers: + if not iocb.ioComplete.isSet(): + if _debug: IOGroup._debug(" - waiting for child: %r", iocb) + break + else: + if _debug: IOGroup._debug(" - all children complete") + # everything complete + self.ioState = COMPLETED + self.trigger() + + def abort(self, err): + """Called by a client to abort all of the member transactions. + When the last pending member is aborted the group callback + function will be called.""" + if _debug: IOGroup._debug("abort %r", err) + + # change the state to reflect that it was killed + self.ioState = ABORTED + self.ioError = err + + # abort all the members + for iocb in self.ioMembers: + iocb.abort(err) + + # notify the client + self.trigger() + +# +# IOQueue +# + +@bacpypes_debugging +class IOQueue: + + def __init__(self, name=None): + if _debug: IOQueue._debug("__init__ %r", name) + + self.notempty = threading.Event() + self.notempty.clear() + + self.queue = [] + + def put(self, iocb): + """Add an IOCB to a queue. This is usually called by the function + that filters requests and passes them out to the correct processing + thread.""" + if _debug: IOQueue._debug("put %r", iocb) + + # requests should be pending before being queued + if iocb.ioState != PENDING: + raise RuntimeError("invalid state transition") + + # save that it might have been empty + wasempty = not self.notempty.isSet() + + # add the request to the end of the list of iocb's at same priority + priority = iocb.ioPriority + item = (priority, iocb) + self.queue.insert(bisect_left(self.queue, (priority+1,)), item) + + # point the iocb back to this queue + iocb.ioQueue = self + + # set the event, queue is no longer empty + self.notempty.set() + + return wasempty + + def get(self, block=1, delay=None): + """Get a request from a queue, optionally block until a request + is available.""" + if _debug: IOQueue._debug("get block=%r delay=%r", block, delay) + + # if the queue is empty and we do not block return None + if not block and not self.notempty.isSet(): + return None + + # wait for something to be in the queue + if delay: + self.notempty.wait(delay) + if not self.notempty.isSet(): + return None + else: + self.notempty.wait() + + # extract the first element + priority, iocb = self.queue[0] + del self.queue[0] + iocb.ioQueue = None + + # if the queue is empty, clear the event + qlen = len(self.queue) + if not qlen: + self.notempty.clear() + + # return the request + return iocb + + def remove(self, iocb): + """Remove a control block from the queue, called if the request + is canceled/aborted.""" + if _debug: IOQueue._debug("remove %r", iocb) + + # remove the request from the queue + for i, item in enumerate(self.queue): + if iocb is item[1]: + if _debug: IOQueue._debug(" - found at %d", i) + del self.queue[i] + + # if the queue is empty, clear the event + qlen = len(self.queue) + if not qlen: + self.notempty.clear() + + # record the new length + # self.queuesize.Record( qlen, _time() ) + break + else: + if _debug: IOQueue._debug(" - not found") + + def abort(self, err): + """Abort all of the control blocks in the queue.""" + if _debug: IOQueue._debug("abort %r", err) + + # send aborts to all of the members + try: + for iocb in self.queue: + iocb.ioQueue = None + iocb.abort(err) + + # flush the queue + self.queue = [] + + # the queue is now empty, clear the event + self.notempty.clear() + except ValueError: + pass + +# +# IOController +# + +@bacpypes_debugging +class IOController(object): + + def __init__(self, name=None): + """Initialize a controller.""" + if _debug: IOController._debug("__init__ name=%r", name) + + # save the name + self.name = name + + def abort(self, err): + """Abort all requests, no default implementation.""" + pass + + def request_io(self, iocb): + """Called by a client to start processing a request.""" + if _debug: IOController._debug("request_io %r", iocb) + + # check that the parameter is an IOCB + if not isinstance(iocb, IOCB): + raise TypeError("IOCB expected") + + # bind the iocb to this controller + iocb.ioController = self + + try: + # hopefully there won't be an error + err = None + + # change the state + iocb.ioState = PENDING + + # let derived class figure out how to process this + self.process_io(iocb) + except: + # extract the error + err = sys.exc_info()[1] + + # if there was an error, abort the request + if err: + self.abort_io(iocb, err) + + def process_io(self, iocb): + """Figure out how to respond to this request. This must be + provided by the derived class.""" + raise NotImplementedError("IOController must implement process_io()") + + def active_io(self, iocb): + """Called by a handler to notify the controller that a request is + being processed.""" + if _debug: IOController._debug("active_io %r", iocb) + + # requests should be idle or pending before coming active + if (iocb.ioState != IDLE) and (iocb.ioState != PENDING): + raise RuntimeError("invalid state transition (currently %d)" % (iocb.ioState,)) + + # change the state + iocb.ioState = ACTIVE + + def complete_io(self, iocb, msg): + """Called by a handler to return data to the client.""" + if _debug: IOController._debug("complete_io %r %r", iocb, msg) + + # if it completed, leave it alone + if iocb.ioState == COMPLETED: + pass + + # if it already aborted, leave it alone + elif iocb.ioState == ABORTED: + pass + + else: + # change the state + iocb.ioState = COMPLETED + iocb.ioResponse = msg + + # notify the client + iocb.trigger() + + def abort_io(self, iocb, err): + """Called by a handler or a client to abort a transaction.""" + if _debug: IOController._debug("abort_io %r %r", iocb, err) + + # if it completed, leave it alone + if iocb.ioState == COMPLETED: + pass + + # if it already aborted, leave it alone + elif iocb.ioState == ABORTED: + pass + + else: + # change the state + iocb.ioState = ABORTED + iocb.ioError = err + + # notify the client + iocb.trigger() + +# +# IOQController +# + +@bacpypes_debugging +class IOQController(IOController): + + wait_time = 0.0 + + def __init__(self, name=None): + """Initialize a queue controller.""" + if _debug: IOQController._debug("__init__ name=%r", name) + IOController.__init__(self, name) + + # start idle + self.state = CTRL_IDLE + _statelog.debug("%s %s %s" % (_strftime(), self.name, "idle")) + + # no active iocb + self.active_iocb = None + + # create an IOQueue for iocb's requested when not idle + self.ioQueue = IOQueue(str(name) + " queue") + + def abort(self, err): + """Abort all pending requests.""" + if _debug: IOQController._debug("abort %r", err) + + if (self.state == CTRL_IDLE): + if _debug: IOQController._debug(" - idle") + return + + while True: + iocb = self.ioQueue.get() + if not iocb: + break + if _debug: IOQController._debug(" - iocb: %r", iocb) + + # change the state + iocb.ioState = ABORTED + iocb.ioError = err + + # notify the client + iocb.trigger() + + if (self.state != CTRL_IDLE): + if _debug: IOQController._debug(" - busy after aborts") + + def request_io(self, iocb): + """Called by a client to start processing a request.""" + if _debug: IOQController._debug("request_io %r", iocb) + + # bind the iocb to this controller + iocb.ioController = self + + # if we're busy, queue it + if (self.state != CTRL_IDLE): + if _debug: IOQController._debug(" - busy, request queued") + + iocb.ioState = PENDING + self.ioQueue.put(iocb) + return + + try: + # hopefully there won't be an error + err = None + + # let derived class figure out how to process this + self.process_io(iocb) + except: + # extract the error + err = sys.exc_info()[1] + + # if there was an error, abort the request + if err: + self.abort_io(iocb, err) + + def process_io(self, iocb): + """Figure out how to respond to this request. This must be + provided by the derived class.""" + raise NotImplementedError("IOController must implement process_io()") + + def active_io(self, iocb): + """Called by a handler to notify the controller that a request is + being processed.""" + if _debug: IOQController._debug("active_io %r", iocb) + + # base class work first, setting iocb state and timer data + IOController.active_io(self, iocb) + + # change our state + self.state = CTRL_ACTIVE + _statelog.debug("%s %s %s" % (_strftime(), self.name, "active")) + + # keep track of the iocb + self.active_iocb = iocb + + def complete_io(self, iocb, msg): + """Called by a handler to return data to the client.""" + if _debug: IOQController._debug("complete_io %r %r", iocb, msg) + + # check to see if it is completing the active one + if iocb is not self.active_iocb: + raise RuntimeError("not the current iocb") + + # normal completion + IOController.complete_io(self, iocb, msg) + + # no longer an active iocb + self.active_iocb = None + + # check to see if we should wait a bit + if self.wait_time: + # change our state + self.state = CTRL_WAITING + _statelog.debug("%s %s %s" % (_strftime(), self.name, "waiting")) + + # schedule a call in the future + task = FunctionTask(IOQController._wait_trigger, self) + task.install_task(_time() + self.wait_time) + + else: + # change our state + self.state = CTRL_IDLE + _statelog.debug("%s %s %s" % (_strftime(), self.name, "idle")) + + # look for more to do + deferred(IOQController._trigger, self) + + def abort_io(self, iocb, err): + """Called by a handler or a client to abort a transaction.""" + if _debug: IOQController._debug("abort_io %r %r", iocb, err) + + # normal abort + IOController.abort_io(self, iocb, err) + + # check to see if it is completing the active one + if iocb is not self.active_iocb: + if _debug: IOQController._debug(" - not current iocb") + return + + # no longer an active iocb + self.active_iocb = None + + # change our state + self.state = CTRL_IDLE + _statelog.debug("%s %s %s" % (_strftime(), self.name, "idle")) + + # look for more to do + deferred(IOQController._trigger, self) + + def _trigger(self): + """Called to launch the next request in the queue.""" + if _debug: IOQController._debug("_trigger") + + # if we are busy, do nothing + if self.state != CTRL_IDLE: + if _debug: IOQController._debug(" - not idle") + return + + # if there is nothing to do, return + if not self.ioQueue.queue: + if _debug: IOQController._debug(" - empty queue") + return + + # get the next iocb + iocb = self.ioQueue.get() + + try: + # hopefully there won't be an error + err = None + + # let derived class figure out how to process this + self.process_io(iocb) + except: + # extract the error + err = sys.exc_info()[1] + + # if there was an error, abort the request + if err: + self.abort_io(iocb, err) + + # if we're idle, call again + if self.state == CTRL_IDLE: + deferred(IOQController._trigger, self) + + def _wait_trigger(self): + """Called to launch the next request in the queue.""" + if _debug: IOQController._debug("_wait_trigger") + + # make sure we are waiting + if (self.state != CTRL_WAITING): + raise RuntimeError("not waiting") + + # change our state + self.state = CTRL_IDLE + _statelog.debug("%s %s %s" % (_strftime(), self.name, "idle")) + + # look for more to do + IOQController._trigger(self) + +# +# ClientController +# + +@bacpypes_debugging +class ClientController(Client, IOQController): + + def __init__(self): + if _debug: ClientController._debug("__init__") + Client.__init__(self) + IOController.__init__(self) + + def process_io(self, iocb): + if _debug: ClientController._debug("process_io %r", iocb) + + # this is now an active request + self.active_io(iocb) + + # send the PDU downstream + self.request(iocb.args[0]) + + def confirmation(self, pdu): + if _debug: ClientController._debug("confirmation %r %r", args, kwargs) + + # make sure it has an active iocb + if not self.active_iocb: + ClientController._debug("no active request") + return + + # look for exceptions + if isinstance(pdu, Exception): + self.abort_io(self.active_iocb, pdu) + else: + self.complete_io(self.active_iocb, pdu) + +# +# SieveQueue +# + +@bacpypes_debugging +class SieveQueue(IOQController): + + def __init__(self, request_fn, address=None): + if _debug: SieveQueue._debug("__init__ %r %r", request_fn, address) + IOQController.__init__(self, str(address)) + + # save a reference to the request function + self.request_fn = request_fn + self.address = address + + def process_io(self, iocb): + if _debug: SieveQueue._debug("process_io %r", iocb) + + # this is now an active request + self.active_io(iocb) + + # send the request + self.request_fn(iocb.args[0]) + +# +# SieveClientController +# + +@bacpypes_debugging +class SieveClientController(Client, IOController): + + def __init__(self): + if _debug: SieveClientController._debug("__init__") + Client.__init__(self) + IOController.__init__(self) + + # queues for each address + self.queues = {} + + def process_io(self, iocb): + if _debug: SieveClientController._debug("process_io %r", iocb) + + # get the destination address from the pdu + destination_address = iocb.args[0].pduDestination + if _debug: SieveClientController._debug(" - destination_address: %r", destination_address) + + # look up the queue + queue = self.queues.get(destination_address, None) + if not queue: + queue = SieveQueue(self.request, destination_address) + self.queues[destination_address] = queue + if _debug: SieveClientController._debug(" - queue: %r", queue) + + # ask the queue to process the request + queue.request_io(iocb) + + def request(self, pdu): + if _debug: SieveClientController._debug("request %r", pdu) + + # send it downstream + super(SieveClientController, self).request(pdu) + + def confirmation(self, pdu): + if _debug: SieveClientController._debug("confirmation %r", pdu) + + # get the source address + source_address = pdu.pduSource + if _debug: SieveClientController._debug(" - source_address: %r", source_address) + + # look up the queue + queue = self.queues.get(source_address, None) + if not queue: + SieveClientController._debug("no queue for %r" % (source_address,)) + return + if _debug: SieveClientController._debug(" - queue: %r", queue) + + # make sure it has an active iocb + if not queue.active_iocb: + SieveClientController._debug("no active request for %r" % (source_address,)) + return + + # complete the request + if isinstance(pdu, Exception): + queue.abort_io(queue.active_iocb, pdu) + else: + queue.complete_io(queue.active_iocb, pdu) + + # if the queue is empty and idle, forget about the controller + if not queue.ioQueue.queue and not queue.active_iocb: + if _debug: SieveClientController._debug(" - queue is empty") + del self.queues[source_address] + +# +# register_controller +# + +@bacpypes_debugging +def register_controller(controller): + if _debug: register_controller._debug("register_controller %r", controller) + global local_controllers + + # skip those that shall not be named + if not controller.name: + return + + # make sure there isn't one already + if controller.name in local_controllers: + raise RuntimeError("already a local controller named %r" % (controller.name,)) + + local_controllers[controller.name] = controller + +# +# abort +# + +@bacpypes_debugging +def abort(err): + """Abort everything, everywhere.""" + if _debug: abort._debug("abort %r", err) + global local_controllers + + # tell all the local controllers to abort + for controller in local_controllers.values(): + controller.abort(err) diff --git a/py34/bacpypes/object.py b/py34/bacpypes/object.py index 7c7f988e..130a44da 100755 --- a/py34/bacpypes/object.py +++ b/py34/bacpypes/object.py @@ -5,6 +5,8 @@ """ import sys +from copy import copy as _copy +from collections import defaultdict from .errors import ConfigurationError, ExecutionError, \ InvalidParameterDatatype @@ -173,8 +175,11 @@ def ReadProperty(self, obj, arrayIndex=None): raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') if value is not None: - # dive in, the water's fine - value = value[arrayIndex] + try: + # dive in, the water's fine + value = value[arrayIndex] + except IndexError: + raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') # all set return value @@ -210,6 +215,9 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False self.identifier, self.datatype.__name__, )) + # local check if the property is monitored + is_monitored = self.identifier in obj._property_monitors + if arrayIndex is not None: if not issubclass(self.datatype, Array): raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') @@ -219,14 +227,34 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False if arry is None: raise RuntimeError("%s uninitialized array" % (self.identifier,)) + if is_monitored: + old_value = _copy(arry) + # seems to be OK, let the array object take over if _debug: Property._debug(" - forwarding to array") - arry[arrayIndex] = value + try: + arry[arrayIndex] = value + except IndexError: + raise ExecutionError(errorClass='property', errorCode='invalidArrayIndex') - return + # check for monitors, call each one with the old and new value + if is_monitored: + for fn in obj._property_monitors[self.identifier]: + if _debug: Property._debug(" - monitor: %r", fn) + fn(old_value, arry) + + else: + if is_monitored: + old_value = obj._values.get(self.identifier, None) - # seems to be OK - obj._values[self.identifier] = value + # seems to be OK + obj._values[self.identifier] = value + + # check for monitors, call each one with the old and new value + if is_monitored: + for fn in obj._property_monitors[self.identifier]: + if _debug: Property._debug(" - monitor: %r", fn) + fn(old_value, value) # # StandardProperty @@ -365,6 +393,9 @@ def __init__(self, **kwargs): # start with a clean dict of values self._values = {} + # empty list of property monitors + self._property_monitors = defaultdict(list) + # start with a clean array of property identifiers if 'propertyList' in initargs: propertyList = None @@ -440,6 +471,49 @@ def __setattr__(self, attr, value): return prop.WriteProperty(self, value, direct=True) + def add_property(self, prop): + """Add a property to an object. The property is an instance of + a Property or one of its derived classes. Adding a property + disconnects it from the collection of properties common to all of the + objects of its class.""" + if _debug: Object._debug("add_property %r", prop) + + # make a copy of the properties dictionary + self._properties = _copy(self._properties) + + # save the property reference and default value (usually None) + self._properties[prop.identifier] = prop + self._values[prop.identifier] = prop.default + + # tell the object it has a new property + if 'propertyList' in self._values: + property_list = self.propertyList + if prop.identifier not in property_list: + if _debug: Object._debug(" - adding to property list") + property_list.append(prop.identifier) + + def delete_property(self, prop): + """Delete a property from an object. The property is an instance of + a Property or one of its derived classes, but only the property + is relavent. Deleting a property disconnects it from the collection of + properties common to all of the objects of its class.""" + if _debug: Object._debug("delete_property %r", value) + + # make a copy of the properties dictionary + self._properties = _copy(self._properties) + + # delete the property from the dictionary and values + del self._properties[prop.identifier] + if prop.identifier in self._values: + del self._values[prop.identifier] + + # remove the property identifier from its list of know properties + if 'propertyList' in self._values: + property_list = self.propertyList + if prop.identifier in property_list: + if _debug: Object._debug(" - removing from property list") + property_list.remove(prop.identifier) + def ReadProperty(self, propid, arrayIndex=None): if _debug: Object._debug("ReadProperty %r arrayIndex=%r", propid, arrayIndex) @@ -582,9 +656,9 @@ class AccessCredentialObject(Object): , OptionalProperty('extendedTimeEnable', Boolean) , OptionalProperty('authorizationExemptions', SequenceOf(AuthorizationException)) , OptionalProperty('reliabilityEvaluationInhibit', Boolean) - , OptionalProperty('masterExemption', Boolean) - , OptionalProperty('passbackExemption', Boolean) - , OptionalProperty('occupancyExemption', Boolean) +# , OptionalProperty('masterExemption', Boolean) +# , OptionalProperty('passbackExemption', Boolean) +# , OptionalProperty('occupancyExemption', Boolean) ] @register_object_type @@ -1266,6 +1340,7 @@ class DeviceObject(Object): , OptionalProperty('timeSynchronizationInterval', Unsigned) , OptionalProperty('alignIntervals', Boolean) , OptionalProperty('intervalOffset', Unsigned) + , OptionalProperty('serialNumber', CharacterString) ] @register_object_type diff --git a/py34/bacpypes/primitivedata.py b/py34/bacpypes/primitivedata.py index 22770ed0..66aefbad 100755 --- a/py34/bacpypes/primitivedata.py +++ b/py34/bacpypes/primitivedata.py @@ -1253,7 +1253,7 @@ class Date(Atomic): def __init__(self, arg=None, year=255, month=255, day=255, day_of_week=255): self.value = (year, month, day, day_of_week) - + if arg is None: pass elif isinstance(arg, Tag): @@ -1371,7 +1371,7 @@ def CalcDayOfWeek(self): elif day in _special_day_inv: pass else: - try: + try: today = time.mktime( (year + 1900, month, day, 0, 0, 0, 0, 0, -1) ) day_of_week = time.gmtime(today)[6] + 1 except OverflowError: diff --git a/py34/bacpypes/service/__init__.py b/py34/bacpypes/service/__init__.py new file mode 100644 index 00000000..69329988 --- /dev/null +++ b/py34/bacpypes/service/__init__.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python + +""" +Service Subpackage +""" + +from . import test +from . import detect + +from . import device +from . import object +from . import cov +from . import file diff --git a/py34/bacpypes/service/cov.py b/py34/bacpypes/service/cov.py new file mode 100644 index 00000000..8da0c92c --- /dev/null +++ b/py34/bacpypes/service/cov.py @@ -0,0 +1,638 @@ +#!/usr/bin/env python + +""" +Change Of Value Service +""" + +from ..debugging import bacpypes_debugging, DebugContents, ModuleLogger +from ..capability import Capability + +from ..task import OneShotTask, TaskManager +from ..iocb import IOCB + +from ..basetypes import DeviceAddress, COVSubscription, PropertyValue, \ + Recipient, RecipientProcess, ObjectPropertyReference +from ..constructeddata import SequenceOf, Any +from ..apdu import ConfirmedCOVNotificationRequest, \ + UnconfirmedCOVNotificationRequest, \ + SimpleAckPDU, Error, RejectPDU, AbortPDU +from ..errors import ExecutionError + +from ..object import Property +from .detect import DetectionAlgorithm, monitor_filter + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# SubscriptionList +# + +@bacpypes_debugging +class SubscriptionList: + + def __init__(self): + if _debug: SubscriptionList._debug("__init__") + + self.cov_subscriptions = [] + + def append(self, cov): + if _debug: SubscriptionList._debug("append %r", cov) + + self.cov_subscriptions.append(cov) + + def remove(self, cov): + if _debug: SubscriptionList._debug("remove %r", cov) + + self.cov_subscriptions.remove(cov) + + def find(self, client_addr, proc_id, obj_id): + if _debug: SubscriptionList._debug("find %r %r %r", client_addr, proc_id, obj_id) + + for cov in self.cov_subscriptions: + all_equal = (cov.client_addr == client_addr) and \ + (cov.proc_id == proc_id) and \ + (cov.obj_id == obj_id) + if _debug: SubscriptionList._debug(" - cov, all_equal: %r %r", cov, all_equal) + + if all_equal: + return cov + + return None + + def __len__(self): + if _debug: SubscriptionList._debug("__len__") + + return len(self.cov_subscriptions) + + def __iter__(self): + if _debug: SubscriptionList._debug("__iter__") + + for cov in self.cov_subscriptions: + yield cov + + +# +# Subscription +# + +@bacpypes_debugging +class Subscription(OneShotTask, DebugContents): + + _debug_contents = ( + 'obj_ref', + 'client_addr', + 'proc_id', + 'obj_id', + 'confirmed', + 'lifetime', + ) + + def __init__(self, obj_ref, client_addr, proc_id, obj_id, confirmed, lifetime): + if _debug: Subscription._debug("__init__ %r %r %r %r %r %r", obj_ref, client_addr, proc_id, obj_id, confirmed, lifetime) + OneShotTask.__init__(self) + + # save the reference to the related object + self.obj_ref = obj_ref + + # save the parameters + self.client_addr = client_addr + self.proc_id = proc_id + self.obj_id = obj_id + self.confirmed = confirmed + self.lifetime = lifetime + + # if lifetime is non-zero, schedule the subscription to expire + if lifetime != 0: + self.install_task(delta=self.lifetime) + + def cancel_subscription(self): + if _debug: Subscription._debug("cancel_subscription") + + # suspend the task + self.suspend_task() + + # tell the application to cancel us + self.obj_ref._app.cancel_subscription(self) + + # break the object reference + self.obj_ref = None + + def renew_subscription(self, lifetime): + if _debug: Subscription._debug("renew_subscription") + + # suspend iff scheduled + if self.isScheduled: + self.suspend_task() + + # reschedule the task if its not infinite + if lifetime != 0: + self.install_task(delta=lifetime) + + def process_task(self): + if _debug: Subscription._debug("process_task") + + # subscription is canceled + self.cancel_subscription() + +# +# COVDetection +# + +@bacpypes_debugging +class COVDetection(DetectionAlgorithm): + + properties_tracked = () + properties_reported = () + monitored_property_reference = None + + def __init__(self, obj): + if _debug: COVDetection._debug("__init__ %r", obj) + DetectionAlgorithm.__init__(self) + + # keep track of the object + self.obj = obj + + # build a list of parameters and matching object property references + kwargs = {} + for property_name in self.properties_tracked: + setattr(self, property_name, None) + kwargs[property_name] = (obj, property_name) + + # let the base class set up the bindings and initial values + self.bind(**kwargs) + + # list of all active subscriptions + self.cov_subscriptions = SubscriptionList() + + def execute(self): + if _debug: COVDetection._debug("execute") + + # something changed, send out the notifications + self.send_cov_notifications() + + def send_cov_notifications(self): + if _debug: COVDetection._debug("send_cov_notifications") + + # check for subscriptions + if not len(self.cov_subscriptions): + return + + # get the current time from the task manager + current_time = TaskManager().get_time() + if _debug: COVDetection._debug(" - current_time: %r", current_time) + + # create a list of values + list_of_values = [] + for property_name in self.properties_reported: + if _debug: COVDetection._debug(" - property_name: %r", property_name) + + # get the class + property_datatype = self.obj.get_datatype(property_name) + if _debug: COVDetection._debug(" - property_datatype: %r", property_datatype) + + # build the value + bundle_value = property_datatype(self.obj._values[property_name]) + if _debug: COVDetection._debug(" - bundle_value: %r", bundle_value) + + # bundle it into a sequence + property_value = PropertyValue( + propertyIdentifier=property_name, + value=Any(bundle_value), + ) + + # add it to the list + list_of_values.append(property_value) + if _debug: COVDetection._debug(" - list_of_values: %r", list_of_values) + + # loop through the subscriptions and send out notifications + for cov in self.cov_subscriptions: + if _debug: COVDetection._debug(" - cov: %s", repr(cov)) + + # calculate time remaining + if not cov.lifetime: + time_remaining = 0 + else: + time_remaining = int(cov.taskTime - current_time) + + # make sure it is at least one second + if not time_remaining: + time_remaining = 1 + + # build a request with the correct type + if cov.confirmed: + request = ConfirmedCOVNotificationRequest() + else: + request = UnconfirmedCOVNotificationRequest() + + # fill in the parameters + request.pduDestination = cov.client_addr + request.subscriberProcessIdentifier = cov.proc_id + request.initiatingDeviceIdentifier = self.obj._app.localDevice.objectIdentifier + request.monitoredObjectIdentifier = cov.obj_id + request.timeRemaining = time_remaining + request.listOfValues = list_of_values + if _debug: COVDetection._debug(" - request: %s", repr(request)) + + # let the application send it + self.obj._app.cov_notification(cov, request) + + def __str__(self): + return "<" + self.__class__.__name__ + \ + "(" + ','.join(self.properties_tracked) + ')' + \ + ">" + +class GenericCriteria(COVDetection): + + properties_tracked = ( + 'presentValue', + 'statusFlags', + ) + properties_reported = ( + 'presentValue', + 'statusFlags', + ) + monitored_property_reference = 'presentValue' + + +@bacpypes_debugging +class COVIncrementCriteria(COVDetection): + + properties_tracked = ( + 'presentValue', + 'statusFlags', + 'covIncrement', + ) + properties_reported = ( + 'presentValue', + 'statusFlags', + ) + monitored_property_reference = 'presentValue' + + def __init__(self, obj): + if _debug: COVIncrementCriteria._debug("__init__ %r", obj) + COVDetection.__init__(self, obj) + + # previous reported value + self.previous_reported_value = None + + @monitor_filter('presentValue') + def present_value_filter(self, old_value, new_value): + if _debug: COVIncrementCriteria._debug("present_value_filter %r %r", old_value, new_value) + + # first time around initialize to the old value + if self.previous_reported_value is None: + if _debug: COVIncrementCriteria._debug(" - first value: %r", old_value) + self.previous_reported_value = old_value + + # see if it changed enough to trigger reporting + value_changed = (new_value <= (self.previous_reported_value - self.covIncrement)) \ + or (new_value >= (self.previous_reported_value + self.covIncrement)) + if _debug: COVIncrementCriteria._debug(" - value significantly changed: %r", value_changed) + + return value_changed + + def send_cov_notifications(self): + if _debug: COVIncrementCriteria._debug("send_cov_notifications") + + # when sending out notifications, keep the current value + self.previous_reported_value = self.presentValue + + # continue + COVDetection.send_cov_notifications(self) + + +class AccessDoorCriteria(COVDetection): + + properties_tracked = ( + 'presentValue', + 'statusFlags', + 'doorAlarmState', + ) + properties_reported = ( + 'presentValue', + 'statusFlags', + 'doorAlarmState', + ) + +class AccessPointCriteria(COVDetection): + + properties_tracked = ( + 'accessEventTime', + 'statusFlags', + ) + properties_reported = ( + 'accessEvent', + 'statusFlags', + 'accessEventTag', + 'accessEventTime', + 'accessEventCredential', + 'accessEventAuthenticationFactor', + ) + monitored_property_reference = 'accessEvent' + +class CredentialDataInputCriteria(COVDetection): + + properties_tracked = ( + 'updateTime', + 'statusFlags' + ) + properties_reported = ( + 'presentValue', + 'statusFlags', + 'updateTime', + ) + +class LoadControlCriteria(COVDetection): + + properties_tracked = ( + 'presentValue', + 'statusFlags', + 'requestedShedLevel', + 'startTime', + 'shedDuration', + 'dutyWindow', + ) + properties_reported = ( + 'presentValue', + 'statusFlags', + 'requestedShedLevel', + 'startTime', + 'shedDuration', + 'dutyWindow', + ) + +class PulseConverterCriteria(COVDetection): + + properties_tracked = ( + 'presentValue', + 'statusFlags', + ) + properties_reported = ( + 'presentValue', + 'statusFlags', + ) + +# mapping from object type to appropriate criteria class +criteria_type_map = { + 'accessPoint': AccessPointCriteria, + 'analogInput': COVIncrementCriteria, + 'analogOutput': COVIncrementCriteria, + 'analogValue': COVIncrementCriteria, + 'largeAnalogValue': COVIncrementCriteria, + 'integerValue': COVIncrementCriteria, + 'positiveIntegerValue': COVIncrementCriteria, + 'lightingOutput': COVIncrementCriteria, + 'binaryInput': GenericCriteria, + 'binaryOutput': GenericCriteria, + 'binaryValue': GenericCriteria, + 'lifeSafetyPoint': GenericCriteria, + 'lifeSafetyZone': GenericCriteria, + 'multiStateInput': GenericCriteria, + 'multiStateOutput': GenericCriteria, + 'multiStateValue': GenericCriteria, + 'octetString': GenericCriteria, + 'characterString': GenericCriteria, + 'timeValue': GenericCriteria, + 'dateTimeValue': GenericCriteria, + 'dateValue': GenericCriteria, + 'timePatternValue': GenericCriteria, + 'datePatternValue': GenericCriteria, + 'dateTimePatternValue': GenericCriteria, + 'credentialDataInput': CredentialDataInputCriteria, + 'loadControl': LoadControlCriteria, + 'pulseConverter': PulseConverterCriteria, + } + +# +# ActiveCOVSubscriptions +# + +@bacpypes_debugging +class ActiveCOVSubscriptions(Property): + + def __init__(self): + Property.__init__( + self, 'activeCovSubscriptions', SequenceOf(COVSubscription), + default=None, optional=True, mutable=False, + ) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: ActiveCOVSubscriptions._debug("ReadProperty %s arrayIndex=%r", obj, arrayIndex) + + # get the current time from the task manager + current_time = TaskManager().get_time() + if _debug: ActiveCOVSubscriptions._debug(" - current_time: %r", current_time) + + # start with an empty sequence + cov_subscriptions = SequenceOf(COVSubscription)() + + # loop through the object and detection list + for obj, cov_detection in self.cov_detections.items(): + for cov in cov_detection.cov_subscriptions: + # calculate time remaining + if not cov.lifetime: + time_remaining = 0 + else: + time_remaining = int(cov.taskTime - current_time) + + # make sure it is at least one second + if not time_remaining: + time_remaining = 1 + + recipient_process = RecipientProcess( + recipient=Recipient( + address=DeviceAddress( + networkNumber=cov.client_addr.addrNet or 0, + macAddress=cov.client_addr.addrAddr, + ), + ), + processIdentifier=cov.proc_id, + ) + + cov_subscription = COVSubscription( + recipient=recipient_process, + monitoredPropertyReference=ObjectPropertyReference( + objectIdentifier=cov.obj_id, + propertyIdentifier=cov_detection.monitored_property_reference, + ), + issueConfirmedNotifications=cov.confirmed, + timeRemaining=time_remaining, + ) + if hasattr(cov_detection, 'covIncrement'): + cov_subscription.covIncrement = cov_detection.covIncrement + if _debug: ActiveCOVSubscriptions._debug(" - cov_subscription: %r", cov_subscription) + + # add the list + cov_subscriptions.append(cov_subscription) + + return cov_subscriptions + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + + +# +# ChangeOfValueServices +# + +@bacpypes_debugging +class ChangeOfValueServices(Capability): + + def __init__(self): + if _debug: ChangeOfValueServices._debug("__init__") + Capability.__init__(self) + + # map from an object to its detection algorithm + self.cov_detections = {} + + # if there is a local device object, make sure it has an active COV + # subscriptions property + if self.localDevice and self.localDevice.activeCovSubscriptions is None: + self.localDevice.add_property(ActiveCOVSubscriptions()) + + def add_subscription(self, cov): + if _debug: ChangeOfValueServices._debug("add_subscription %r", cov) + + # add it to the subscription list for its object + self.cov_detections[cov.obj_ref].cov_subscriptions.append(cov) + + def cancel_subscription(self, cov): + if _debug: ChangeOfValueServices._debug("cancel_subscription %r", cov) + + # cancel the subscription timeout + if cov.isScheduled: + cov.suspend_task() + if _debug: ChangeOfValueServices._debug(" - task suspended") + + # get the detection algorithm object + cov_detection = self.cov_detections[cov.obj_ref] + + # remove it from the subscription list for its object + cov_detection.cov_subscriptions.remove(cov) + + # if the detection algorithm doesn't have any subscriptions, remove it + if not len(cov_detection.cov_subscriptions): + if _debug: ChangeOfValueServices._debug(" - no more subscriptions") + + # unbind all the hooks into the object + cov_detection.unbind() + + # delete it from the object map + del self.cov_detections[cov.obj_ref] + + def cov_notification(self, cov, request): + if _debug: ChangeOfValueServices._debug("cov_notification %s %s", str(cov), str(request)) + + # create an IOCB with the request + iocb = IOCB(request) + if _debug: ChangeOfValueServices._debug(" - iocb: %r", iocb) + + # add a callback for the response, even if it was unconfirmed + iocb.cov = cov + iocb.add_callback(self.cov_confirmation) + + # send the request via the ApplicationIOController + self.request_io(iocb) + + def cov_confirmation(self, iocb): + if _debug: ChangeOfValueServices._debug("cov_confirmation %r", iocb) + + # do something for success + if iocb.ioResponse: + if _debug: ChangeOfValueServices._debug(" - ack") + self.cov_ack(iocb.cov, iocb.args[0], iocb.ioResponse) + + elif isinstance(iocb.ioError, Error): + if _debug: ChangeOfValueServices._debug(" - error: %r", iocb.ioError.errorCode) + self.cov_error(iocb.cov, iocb.args[0], iocb.ioError) + + elif isinstance(iocb.ioError, RejectPDU): + if _debug: ChangeOfValueServices._debug(" - reject: %r", iocb.ioError.apduAbortRejectReason) + self.cov_reject(iocb.cov, iocb.args[0], iocb.ioError) + + elif isinstance(iocb.ioError, AbortPDU): + if _debug: ChangeOfValueServices._debug(" - abort: %r", iocb.ioError.apduAbortRejectReason) + self.cov_abort(iocb.cov, iocb.args[0], iocb.ioError) + + def cov_ack(self, cov, request, response): + if _debug: ChangeOfValueServices._debug("cov_ack %r %r %r", cov, request, response) + + def cov_error(self, cov, request, response): + if _debug: ChangeOfValueServices._debug("cov_error %r %r %r", cov, request, response) + + def cov_reject(self, cov, request, response): + if _debug: ChangeOfValueServices._debug("cov_reject %r %r %r", cov, request, response) + + def cov_abort(self, cov, request, response): + if _debug: ChangeOfValueServices._debug("cov_abort %r %r %r", cov, request, response) + + ### delete the rest of the pending requests for this client + + def do_SubscribeCOVRequest(self, apdu): + if _debug: ChangeOfValueServices._debug("do_SubscribeCOVRequest %r", apdu) + + # extract the pieces + client_addr = apdu.pduSource + proc_id = apdu.subscriberProcessIdentifier + obj_id = apdu.monitoredObjectIdentifier + confirmed = apdu.issueConfirmedNotifications + lifetime = apdu.lifetime + + # request is to cancel the subscription + cancel_subscription = (confirmed is None) and (lifetime is None) + + # find the object + obj = self.get_object_id(obj_id) + if _debug: ChangeOfValueServices._debug(" - object: %r", obj) + if not obj: + raise ExecutionError(errorClass='object', errorCode='unknownObject') + + # look for an algorithm already associated with this object + cov_detection = self.cov_detections.get(obj, None) + + # if there isn't one, make one and associate it with the object + if not cov_detection: + # look for an associated class and if it's not there it's not supported + criteria_class = criteria_type_map.get(obj_id[0], None) + if not criteria_class: + raise ExecutionError(errorClass='services', errorCode='covSubscriptionFailed') + + # make one of these and bind it to the object + cov_detection = criteria_class(obj) + + # keep track of it for other subscriptions + self.cov_detections[obj] = cov_detection + if _debug: ChangeOfValueServices._debug(" - cov_detection: %r", cov_detection) + + # can a match be found? + cov = cov_detection.cov_subscriptions.find(client_addr, proc_id, obj_id) + if _debug: ChangeOfValueServices._debug(" - cov: %r", cov) + + # if a match was found, update the subscription + if cov: + if cancel_subscription: + if _debug: ChangeOfValueServices._debug(" - cancel the subscription") + self.cancel_subscription(cov) + else: + if _debug: ChangeOfValueServices._debug(" - renew the subscription") + cov.renew_subscription(lifetime) + else: + if cancel_subscription: + if _debug: ChangeOfValueServices._debug(" - cancel a subscription that doesn't exist") + else: + if _debug: ChangeOfValueServices._debug(" - create a subscription") + + # make a subscription + cov = Subscription(obj, client_addr, proc_id, obj_id, confirmed, lifetime) + if _debug: ChangeOfValueServices._debug(" - cov: %r", cov) + + # add it to our subscriptions lists + self.add_subscription(cov) + + # success + response = SimpleAckPDU(context=apdu) + + # return the result + self.response(response) diff --git a/py34/bacpypes/service/detect.py b/py34/bacpypes/service/detect.py new file mode 100755 index 00000000..0be05513 --- /dev/null +++ b/py34/bacpypes/service/detect.py @@ -0,0 +1,144 @@ +#!/usr/bin/env python + +""" +Detection +""" + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger + +from bacpypes.core import deferred + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# DetectionMonitor +# + +@bacpypes_debugging +class DetectionMonitor: + + def __init__(self, algorithm, parameter, obj, prop, filter=None): + if _debug: DetectionMonitor._debug("__init__ ...") + + # keep track of the parameter values + self.algorithm = algorithm + self.parameter = parameter + self.obj = obj + self.prop = prop + self.filter = None + + def property_change(self, old_value, new_value): + if _debug: DetectionMonitor._debug("property_change %r %r", old_value, new_value) + + # set the parameter value + setattr(self.algorithm, self.parameter, new_value) + + # if the algorithm is already triggered, don't bother checking for more + if self.algorithm._triggered: + if _debug: DetectionMonitor._debug(" - already triggered") + return + + # if there is a special filter, use it, otherwise use != + if self.filter: + trigger = self.filter(old_value, new_value) + else: + trigger = (old_value != new_value) + if _debug: DetectionMonitor._debug(" - trigger: %r", trigger) + + # trigger it + if trigger: + deferred(self.algorithm._execute) + if _debug: DetectionMonitor._debug(" - deferred: %r", self.algorithm._execute) + + self.algorithm._triggered = True + +# +# monitor_filter +# + +def monitor_filter(parameter): + def transfer_filter_decorator(fn): + fn._monitor_filter = parameter + return fn + + return transfer_filter_decorator + +# +# DetectionAlgorithm +# + +@bacpypes_debugging +class DetectionAlgorithm: + + def __init__(self): + if _debug: DetectionAlgorithm._debug("__init__") + + # monitor objects + self._monitors = [] + + # triggered flag, set when a parameter changed and the monitor + # schedules the algorithm to execute + self._triggered = False + + def bind(self, **kwargs): + if _debug: DetectionAlgorithm._debug("bind %r", kwargs) + + # build a map of methods that are filters. These have been decorated + # with monitor_filter, but they are unbound methods (or simply + # functions in Python3) at the time they are decorated but by looking + # for them now they are bound to this instance. + monitor_filters = {} + for attr_name in dir(self): + attr = getattr(self, attr_name) + if hasattr(attr, "_monitor_filter"): + monitor_filters[attr._monitor_filter] = attr + if _debug: DetectionAlgorithm._debug(" - monitor_filters: %r", monitor_filters) + + for parameter, (obj, prop) in kwargs.items(): + if not hasattr(self, parameter): + if _debug: DetectionAlgorithm._debug(" - no matching parameter: %r", parameter) + + # make a detection monitor + monitor = DetectionMonitor(self, parameter, obj, prop) + if _debug: DetectionAlgorithm._debug(" - monitor: %r", monitor) + + # check to see if there is a custom filter for it + if parameter in monitor_filters: + monitor.filter = monitor_filters[parameter] + + # keep track of all of these objects for if/when we unbind + self._monitors.append(monitor) + + # add the property value monitor function + obj._property_monitors[prop].append(monitor.property_change) + + # set the parameter value to the property value if it's not None + property_value = obj._values[prop] + if property_value is not None: + if _debug: DetectionAlgorithm._debug(" - %s: %r", parameter, property_value) + setattr(self, parameter, property_value) + + def unbind(self): + if _debug: DetectionAlgorithm._debug("unbind") + + # remove the property value monitor functions + for monitor in self._monitors: + if _debug: DetectionAlgorithm._debug(" - monitor: %r", monitor) + monitor.obj._property_monitors[monitor.prop].remove(monitor.property_change) + + # abandon the array + self._monitors = [] + + def _execute(self): + if _debug: DetectionAlgorithm._debug("_execute") + + # provided by the derived class + self.execute() + + # turn the trigger off + self._triggered = False + + def execute(self): + raise NotImplementedError("execute not implemented") diff --git a/py34/bacpypes/service/device.py b/py34/bacpypes/service/device.py new file mode 100644 index 00000000..cc257ae0 --- /dev/null +++ b/py34/bacpypes/service/device.py @@ -0,0 +1,339 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger +from ..capability import Capability + +from ..pdu import GlobalBroadcast +from ..primitivedata import Date, Time, ObjectIdentifier +from ..constructeddata import ArrayOf + +from ..apdu import WhoIsRequest, IAmRequest, IHaveRequest +from ..errors import ExecutionError, InconsistentParameters, \ + MissingRequiredParameter, ParameterOutOfRange +from ..object import register_object_type, registered_object_types, \ + Property, DeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# CurrentDateProperty +# + +class CurrentDateProperty(Property): + + def __init__(self, identifier): + Property.__init__(self, identifier, Date, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + # access an array + if arrayIndex is not None: + raise TypeError("{0} is unsubscriptable".format(self.identifier)) + + # get the value + now = Date() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# CurrentTimeProperty +# + +class CurrentTimeProperty(Property): + + def __init__(self, identifier): + Property.__init__(self, identifier, Time, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + # access an array + if arrayIndex is not None: + raise TypeError("{0} is unsubscriptable".format(self.identifier)) + + # get the value + now = Time() + now.now() + return now.value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None): + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +# +# LocalDeviceObject +# + +@bacpypes_debugging +class LocalDeviceObject(DeviceObject): + + properties = \ + [ CurrentTimeProperty('localTime') + , CurrentDateProperty('localDate') + ] + + defaultProperties = \ + { 'maxApduLengthAccepted': 1024 + , 'segmentationSupported': 'segmentedBoth' + , 'maxSegmentsAccepted': 16 + , 'apduSegmentTimeout': 5000 + , 'apduTimeout': 3000 + , 'numberOfApduRetries': 3 + } + + def __init__(self, **kwargs): + if _debug: LocalDeviceObject._debug("__init__ %r", kwargs) + + # fill in default property values not in kwargs + for attr, value in LocalDeviceObject.defaultProperties.items(): + if attr not in kwargs: + kwargs[attr] = value + + # check for registration + if self.__class__ not in registered_object_types.values(): + if 'vendorIdentifier' not in kwargs: + raise RuntimeError("vendorIdentifier required to auto-register the LocalDeviceObject class") + register_object_type(self.__class__, vendor_id=kwargs['vendorIdentifier']) + + # check for local time + if 'localDate' in kwargs: + raise RuntimeError("localDate is provided by LocalDeviceObject and cannot be overridden") + if 'localTime' in kwargs: + raise RuntimeError("localTime is provided by LocalDeviceObject and cannot be overridden") + + # check for a minimum value + if kwargs['maxApduLengthAccepted'] < 50: + raise ValueError("invalid max APDU length accepted") + + # dump the updated attributes + if _debug: LocalDeviceObject._debug(" - updated kwargs: %r", kwargs) + + # proceed as usual + DeviceObject.__init__(self, **kwargs) + + # create a default implementation of an object list for local devices. + # If it is specified in the kwargs, that overrides this default. + if ('objectList' not in kwargs): + self.objectList = ArrayOf(ObjectIdentifier)([self.objectIdentifier]) + + # if the object has a property list and one wasn't provided + # in the kwargs, then it was created by default and the objectList + # property should be included + if ('propertyList' not in kwargs) and self.propertyList: + # make sure it's not already there + if 'objectList' not in self.propertyList: + self.propertyList.append('objectList') + +# +# Who-Is I-Am Services +# + +@bacpypes_debugging +class WhoIsIAmServices(Capability): + + def __init__(self): + if _debug: WhoIsIAmServices._debug("__init__") + Capability.__init__(self) + + def who_is(self, low_limit=None, high_limit=None, address=None): + if _debug: WhoIsIAmServices._debug("who_is") + + # build a request + whoIs = WhoIsRequest() + + # defaults to a global broadcast + if not address: + address = GlobalBroadcast() + + # set the destination + whoIs.pduDestination = address + + # check for consistent parameters + if (low_limit is not None): + if (high_limit is None): + raise MissingRequiredParameter("high_limit required") + if (low_limit < 0) or (low_limit > 4194303): + raise ParameterOutOfRange("low_limit out of range") + + # low limit is fine + whoIs.deviceInstanceRangeLowLimit = low_limit + + if (high_limit is not None): + if (low_limit is None): + raise MissingRequiredParameter("low_limit required") + if (high_limit < 0) or (high_limit > 4194303): + raise ParameterOutOfRange("high_limit out of range") + + # high limit is fine + whoIs.deviceInstanceRangeHighLimit = high_limit + + if _debug: WhoIsIAmServices._debug(" - whoIs: %r", whoIs) + + ### put the parameters someplace where they can be matched when the + ### appropriate I-Am comes in + + # away it goes + self.request(whoIs) + + def do_WhoIsRequest(self, apdu): + """Respond to a Who-Is request.""" + if _debug: WhoIsIAmServices._debug("do_WhoIsRequest %r", apdu) + + # ignore this if there's no local device + if not self.localDevice: + if _debug: WhoIsIAmServices._debug(" - no local device") + return + + # extract the parameters + low_limit = apdu.deviceInstanceRangeLowLimit + high_limit = apdu.deviceInstanceRangeHighLimit + + # check for consistent parameters + if (low_limit is not None): + if (high_limit is None): + raise MissingRequiredParameter("deviceInstanceRangeHighLimit required") + if (low_limit < 0) or (low_limit > 4194303): + raise ParameterOutOfRange("deviceInstanceRangeLowLimit out of range") + if (high_limit is not None): + if (low_limit is None): + raise MissingRequiredParameter("deviceInstanceRangeLowLimit required") + if (high_limit < 0) or (high_limit > 4194303): + raise ParameterOutOfRange("deviceInstanceRangeHighLimit out of range") + + # see we should respond + if (low_limit is not None): + if (self.localDevice.objectIdentifier[1] < low_limit): + return + if (high_limit is not None): + if (self.localDevice.objectIdentifier[1] > high_limit): + return + + # generate an I-Am + self.i_am(address=apdu.pduSource) + + def i_am(self, address=None): + if _debug: WhoIsIAmServices._debug("i_am") + + # this requires a local device + if not self.localDevice: + if _debug: WhoIsIAmServices._debug(" - no local device") + return + + # create a I-Am "response" back to the source + iAm = IAmRequest( + iAmDeviceIdentifier=self.localDevice.objectIdentifier, + maxAPDULengthAccepted=self.localDevice.maxApduLengthAccepted, + segmentationSupported=self.localDevice.segmentationSupported, + vendorID=self.localDevice.vendorIdentifier, + ) + + # defaults to a global broadcast + if not address: + address = GlobalBroadcast() + iAm.pduDestination = address + if _debug: WhoIsIAmServices._debug(" - iAm: %r", iAm) + + # away it goes + self.request(iAm) + + def do_IAmRequest(self, apdu): + """Respond to an I-Am request.""" + if _debug: WhoIsIAmServices._debug("do_IAmRequest %r", apdu) + + # check for required parameters + if apdu.iAmDeviceIdentifier is None: + raise MissingRequiredParameter("iAmDeviceIdentifier required") + if apdu.maxAPDULengthAccepted is None: + raise MissingRequiredParameter("maxAPDULengthAccepted required") + if apdu.segmentationSupported is None: + raise MissingRequiredParameter("segmentationSupported required") + if apdu.vendorID is None: + raise MissingRequiredParameter("vendorID required") + + # extract the device instance number + device_instance = apdu.iAmDeviceIdentifier[1] + if _debug: WhoIsIAmServices._debug(" - device_instance: %r", device_instance) + + # extract the source address + device_address = apdu.pduSource + if _debug: WhoIsIAmServices._debug(" - device_address: %r", device_address) + + ### check to see if the application is looking for this device + ### and update the device info cache if it is + +# +# Who-Has I-Have Services +# + +@bacpypes_debugging +class WhoHasIHaveServices(Capability): + + def __init__(self): + if _debug: WhoHasIHaveServices._debug("__init__") + Capability.__init__(self) + + def who_has(self, thing, address=None): + if _debug: WhoHasIHaveServices._debug("who_has %r address=%r", thing, address) + + raise NotImplementedError("who_has") + + def do_WhoHasRequest(self, apdu): + """Respond to a Who-Has request.""" + if _debug: WhoHasIHaveServices._debug("do_WhoHasRequest, %r", apdu) + + # ignore this if there's no local device + if not self.localDevice: + if _debug: WhoIsIAmServices._debug(" - no local device") + return + + # find the object + if apdu.object.objectIdentifier is not None: + obj = self.objectIdentifier.get(apdu.object.objectIdentifier, None) + elif apdu.object.objectName is not None: + obj = self.objectName.get(apdu.object.objectName, None) + else: + raise InconsistentParameters("object identifier or object name required") + if not obj: + raise ExecutionError(errorClass='object', errorCode='unknownObject') + + # send out the response + self.i_have(obj, address=apdu.pduSource) + + def i_have(self, thing, address=None): + if _debug: WhoHasIHaveServices._debug("i_have %r address=%r", thing, address) + + # ignore this if there's no local device + if not self.localDevice: + if _debug: WhoIsIAmServices._debug(" - no local device") + return + + # build the request + iHave = IHaveRequest( + deviceIdentifier=self.localDevice.objectIdentifier, + objectIdentifier=thing.objectIdentifier, + objectName=thing.objectName, + ) + + # defaults to a global broadcast + if not address: + address = GlobalBroadcast() + iHave.pduDestination = address + if _debug: WhoHasIHaveServices._debug(" - iHave: %r", iHave) + + # send it along + self.request(iHave) + + def do_IHaveRequest(self, apdu): + """Respond to a I-Have request.""" + if _debug: WhoHasIHaveServices._debug("do_IHaveRequest %r", apdu) + + # check for required parameters + if apdu.deviceIdentifier is None: + raise MissingRequiredParameter("deviceIdentifier required") + if apdu.objectIdentifier is None: + raise MissingRequiredParameter("objectIdentifier required") + if apdu.objectName is None: + raise MissingRequiredParameter("objectName required") + + ### check to see if the application is looking for this object diff --git a/py34/bacpypes/service/file.py b/py34/bacpypes/service/file.py new file mode 100644 index 00000000..8723b6ff --- /dev/null +++ b/py34/bacpypes/service/file.py @@ -0,0 +1,297 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger +from ..capability import Capability + +from ..object import FileObject + +from ..apdu import AtomicReadFileACK, AtomicReadFileACKAccessMethodChoice, \ + AtomicReadFileACKAccessMethodRecordAccess, \ + AtomicReadFileACKAccessMethodStreamAccess, \ + AtomicWriteFileACK +from ..errors import ExecutionError, MissingRequiredParameter + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# Local Record Access File Object Type +# + +@bacpypes_debugging +class LocalRecordAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a record accessed file object. """ + if _debug: + LocalRecordAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'recordAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'recordAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of records. """ + raise NotImplementedError("__len__") + + def read_record(self, start_record, record_count): + """ Read a number of records starting at a specific record. """ + raise NotImplementedError("read_record") + + def write_record(self, start_record, record_count, record_data): + """ Write a number of records, starting at a specific record. """ + raise NotImplementedError("write_record") + +# +# Local Stream Access File Object Type +# + +@bacpypes_debugging +class LocalStreamAccessFileObject(FileObject): + + def __init__(self, **kwargs): + """ Initialize a stream accessed file object. """ + if _debug: + LocalStreamAccessFileObject._debug("__init__ %r", + kwargs, + ) + + # verify the file access method or provide it + if 'fileAccessMethod' in kwargs: + if kwargs['fileAccessMethod'] != 'streamAccess': + raise ValueError("inconsistent file access method") + else: + kwargs['fileAccessMethod'] = 'streamAccess' + + # continue with initialization + FileObject.__init__(self, **kwargs) + + def __len__(self): + """ Return the number of octets in the file. """ + raise NotImplementedError("write_file") + + def read_stream(self, start_position, octet_count): + """ Read a chunk of data out of the file. """ + raise NotImplementedError("read_stream") + + def write_stream(self, start_position, data): + """ Write a number of octets, starting at a specific offset. """ + raise NotImplementedError("write_stream") + +# +# File Application Mixin +# + +@bacpypes_debugging +class FileServices(Capability): + + def __init__(self): + if _debug: FileServices._debug("__init__") + Capability.__init__(self) + + def do_AtomicReadFileRequest(self, apdu): + """Return one of our records.""" + if _debug: FileServices._debug("do_AtomicReadFileRequest %r", apdu) + + if (apdu.fileIdentifier[0] != 'file'): + raise ExecutionError('services', 'inconsistentObjectType') + + # get the object + obj = self.get_object_id(apdu.fileIdentifier) + if _debug: FileServices._debug(" - object: %r", obj) + + if not obj: + raise ExecutionError('object', 'unknownObject') + + if apdu.accessMethod.recordAccess: + # check against the object + if obj.fileAccessMethod != 'recordAccess': + raise ExecutionError('services', 'invalidFileAccessMethod') + + # simplify + record_access = apdu.accessMethod.recordAccess + + # check for required parameters + if record_access.fileStartRecord is None: + raise MissingRequiredParameter("fileStartRecord required") + if record_access.requestedRecordCount is None: + raise MissingRequiredParameter("requestedRecordCount required") + + ### verify start is valid - double check this (empty files?) + if (record_access.fileStartRecord < 0) or \ + (record_access.fileStartRecord >= len(obj)): + raise ExecutionError('services', 'invalidFileStartPosition') + + # pass along to the object + end_of_file, record_data = obj.read_record( + record_access.fileStartRecord, + record_access.requestedRecordCount, + ) + if _debug: FileServices._debug(" - record_data: %r", record_data) + + # this is an ack + resp = AtomicReadFileACK(context=apdu, + endOfFile=end_of_file, + accessMethod=AtomicReadFileACKAccessMethodChoice( + recordAccess=AtomicReadFileACKAccessMethodRecordAccess( + fileStartRecord=record_access.fileStartRecord, + returnedRecordCount=len(record_data), + fileRecordData=record_data, + ), + ), + ) + + elif apdu.accessMethod.streamAccess: + # check against the object + if obj.fileAccessMethod != 'streamAccess': + raise ExecutionError('services', 'invalidFileAccessMethod') + + # simplify + stream_access = apdu.accessMethod.streamAccess + + # check for required parameters + if stream_access.fileStartPosition is None: + raise MissingRequiredParameter("fileStartPosition required") + if stream_access.requestedOctetCount is None: + raise MissingRequiredParameter("requestedOctetCount required") + + ### verify start is valid - double check this (empty files?) + if (stream_access.fileStartPosition < 0) or \ + (stream_access.fileStartPosition >= len(obj)): + raise ExecutionError('services', 'invalidFileStartPosition') + + # pass along to the object + end_of_file, record_data = obj.read_stream( + stream_access.fileStartPosition, + stream_access.requestedOctetCount, + ) + if _debug: FileServices._debug(" - record_data: %r", record_data) + + # this is an ack + resp = AtomicReadFileACK(context=apdu, + endOfFile=end_of_file, + accessMethod=AtomicReadFileACKAccessMethodChoice( + streamAccess=AtomicReadFileACKAccessMethodStreamAccess( + fileStartPosition=stream_access.fileStartPosition, + fileData=record_data, + ), + ), + ) + + if _debug: FileServices._debug(" - resp: %r", resp) + + # return the result + self.response(resp) + + def do_AtomicWriteFileRequest(self, apdu): + """Return one of our records.""" + if _debug: FileServices._debug("do_AtomicWriteFileRequest %r", apdu) + + if (apdu.fileIdentifier[0] != 'file'): + raise ExecutionError('services', 'inconsistentObjectType') + + # get the object + obj = self.get_object_id(apdu.fileIdentifier) + if _debug: FileServices._debug(" - object: %r", obj) + + if not obj: + raise ExecutionError('object', 'unknownObject') + + if apdu.accessMethod.recordAccess: + # check against the object + if obj.fileAccessMethod != 'recordAccess': + raise ExecutionError('services', 'invalidFileAccessMethod') + + # simplify + record_access = apdu.accessMethod.recordAccess + + # check for required parameters + if record_access.fileStartRecord is None: + raise MissingRequiredParameter("fileStartRecord required") + if record_access.recordCount is None: + raise MissingRequiredParameter("recordCount required") + if record_access.fileRecordData is None: + raise MissingRequiredParameter("fileRecordData required") + + # check for read-only + if obj.readOnly: + raise ExecutionError('services', 'fileAccessDenied') + + # pass along to the object + start_record = obj.write_record( + record_access.fileStartRecord, + record_access.recordCount, + record_access.fileRecordData, + ) + if _debug: FileServices._debug(" - start_record: %r", start_record) + + # this is an ack + resp = AtomicWriteFileACK(context=apdu, + fileStartRecord=start_record, + ) + + elif apdu.accessMethod.streamAccess: + # check against the object + if obj.fileAccessMethod != 'streamAccess': + raise ExecutionError('services', 'invalidFileAccessMethod') + + # simplify + stream_access = apdu.accessMethod.streamAccess + + # check for required parameters + if stream_access.fileStartPosition is None: + raise MissingRequiredParameter("fileStartPosition required") + if stream_access.fileData is None: + raise MissingRequiredParameter("fileData required") + + # check for read-only + if obj.readOnly: + raise ExecutionError('services', 'fileAccessDenied') + + # pass along to the object + start_position = obj.write_stream( + stream_access.fileStartPosition, + stream_access.fileData, + ) + if _debug: FileServices._debug(" - start_position: %r", start_position) + + # this is an ack + resp = AtomicWriteFileACK(context=apdu, + fileStartPosition=start_position, + ) + + if _debug: FileServices._debug(" - resp: %r", resp) + + # return the result + self.response(resp) + +# +# FileServicesClient +# + +class FileServicesClient(Capability): + + def read_record(self, address, fileIdentifier, start_record, record_count): + """ Read a number of records starting at a specific record. """ + raise NotImplementedError("read_record") + + def write_record(self, address, fileIdentifier, start_record, record_count, record_data): + """ Write a number of records, starting at a specific record. """ + raise NotImplementedError("write_record") + + def read_stream(self, address, fileIdentifier, start_position, octet_count): + """ Read a chunk of data out of the file. """ + raise NotImplementedError("read_stream") + + def write_stream(self, address, fileIdentifier, start_position, data): + """ Write a number of octets, starting at a specific offset. """ + raise NotImplementedError("write_stream") diff --git a/py34/bacpypes/service/object.py b/py34/bacpypes/service/object.py new file mode 100755 index 00000000..e453a5b7 --- /dev/null +++ b/py34/bacpypes/service/object.py @@ -0,0 +1,327 @@ +#!/usr/bin/env python + +from ..debugging import bacpypes_debugging, ModuleLogger +from ..capability import Capability + +from ..basetypes import ErrorType +from ..primitivedata import Atomic, Null, Unsigned +from ..constructeddata import Any, Array + +from ..apdu import Error, \ + SimpleAckPDU, ReadPropertyACK, ReadPropertyMultipleACK, \ + ReadAccessResult, ReadAccessResultElement, ReadAccessResultElementChoice +from ..errors import ExecutionError +from ..object import PropertyError + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# ReadProperty and WriteProperty Services +# + +@bacpypes_debugging +class ReadWritePropertyServices(Capability): + + def __init__(self): + if _debug: ReadWritePropertyServices._debug("__init__") + Capability.__init__(self) + + def do_ReadPropertyRequest(self, apdu): + """Return the value of some property of one of our objects.""" + if _debug: ReadWritePropertyServices._debug("do_ReadPropertyRequest %r", apdu) + + # extract the object identifier + objId = apdu.objectIdentifier + + # check for wildcard + if (objId == ('device', 4194303)) and self.localDevice is not None: + if _debug: ReadWritePropertyServices._debug(" - wildcard device identifier") + objId = self.localDevice.objectIdentifier + + # get the object + obj = self.get_object_id(objId) + if _debug: ReadWritePropertyServices._debug(" - object: %r", obj) + + if not obj: + raise ExecutionError(errorClass='object', errorCode='unknownObject') + + try: + # get the datatype + datatype = obj.get_datatype(apdu.propertyIdentifier) + if _debug: ReadWritePropertyServices._debug(" - datatype: %r", datatype) + + # get the value + value = obj.ReadProperty(apdu.propertyIdentifier, apdu.propertyArrayIndex) + if _debug: ReadWritePropertyServices._debug(" - value: %r", value) + if value is None: + raise PropertyError(apdu.propertyIdentifier) + + # change atomic values into something encodeable + if issubclass(datatype, Atomic): + value = datatype(value) + elif issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): + if apdu.propertyArrayIndex == 0: + value = Unsigned(value) + elif issubclass(datatype.subtype, Atomic): + value = datatype.subtype(value) + elif not isinstance(value, datatype.subtype): + raise TypeError("invalid result datatype, expecting {0} and got {1}" \ + .format(datatype.subtype.__name__, type(value).__name__)) + elif not isinstance(value, datatype): + raise TypeError("invalid result datatype, expecting {0} and got {1}" \ + .format(datatype.__name__, type(value).__name__)) + if _debug: ReadWritePropertyServices._debug(" - encodeable value: %r", value) + + # this is a ReadProperty ack + resp = ReadPropertyACK(context=apdu) + resp.objectIdentifier = objId + resp.propertyIdentifier = apdu.propertyIdentifier + resp.propertyArrayIndex = apdu.propertyArrayIndex + + # save the result in the property value + resp.propertyValue = Any() + resp.propertyValue.cast_in(value) + if _debug: ReadWritePropertyServices._debug(" - resp: %r", resp) + + except PropertyError: + raise ExecutionError(errorClass='object', errorCode='unknownProperty') + + # return the result + self.response(resp) + + def do_WritePropertyRequest(self, apdu): + """Change the value of some property of one of our objects.""" + if _debug: ReadWritePropertyServices._debug("do_WritePropertyRequest %r", apdu) + + # get the object + obj = self.get_object_id(apdu.objectIdentifier) + if _debug: ReadWritePropertyServices._debug(" - object: %r", obj) + if not obj: + raise ExecutionError(errorClass='object', errorCode='unknownObject') + + try: + # check if the property exists + if obj.ReadProperty(apdu.propertyIdentifier, apdu.propertyArrayIndex) is None: + raise PropertyError(apdu.propertyIdentifier) + + # get the datatype, special case for null + if apdu.propertyValue.is_application_class_null(): + datatype = Null + else: + datatype = obj.get_datatype(apdu.propertyIdentifier) + if _debug: ReadWritePropertyServices._debug(" - datatype: %r", datatype) + + # special case for array parts, others are managed by cast_out + if issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): + if apdu.propertyArrayIndex == 0: + value = apdu.propertyValue.cast_out(Unsigned) + else: + value = apdu.propertyValue.cast_out(datatype.subtype) + else: + value = apdu.propertyValue.cast_out(datatype) + if _debug: ReadWritePropertyServices._debug(" - value: %r", value) + + # change the value + value = obj.WriteProperty(apdu.propertyIdentifier, value, apdu.propertyArrayIndex, apdu.priority) + + # success + resp = SimpleAckPDU(context=apdu) + if _debug: ReadWritePropertyServices._debug(" - resp: %r", resp) + + except PropertyError: + raise ExecutionError(errorClass='object', errorCode='unknownProperty') + + # return the result + self.response(resp) + +# +# read_property_to_any +# + +@bacpypes_debugging +def read_property_to_any(obj, propertyIdentifier, propertyArrayIndex=None): + """Read the specified property of the object, with the optional array index, + and cast the result into an Any object.""" + if _debug: read_property_to_any._debug("read_property_to_any %s %r %r", obj, propertyIdentifier, propertyArrayIndex) + + # get the datatype + datatype = obj.get_datatype(propertyIdentifier) + if _debug: read_property_to_any._debug(" - datatype: %r", datatype) + if datatype is None: + raise ExecutionError(errorClass='property', errorCode='datatypeNotSupported') + + # get the value + value = obj.ReadProperty(propertyIdentifier, propertyArrayIndex) + if _debug: read_property_to_any._debug(" - value: %r", value) + if value is None: + raise ExecutionError(errorClass='property', errorCode='unknownProperty') + + # change atomic values into something encodeable + if issubclass(datatype, Atomic): + value = datatype(value) + elif issubclass(datatype, Array) and (propertyArrayIndex is not None): + if propertyArrayIndex == 0: + value = Unsigned(value) + elif issubclass(datatype.subtype, Atomic): + value = datatype.subtype(value) + elif not isinstance(value, datatype.subtype): + raise TypeError("invalid result datatype, expecting %s and got %s" \ + % (datatype.subtype.__name__, type(value).__name__)) + elif not isinstance(value, datatype): + raise TypeError("invalid result datatype, expecting %s and got %s" \ + % (datatype.__name__, type(value).__name__)) + if _debug: read_property_to_any._debug(" - encodeable value: %r", value) + + # encode the value + result = Any() + result.cast_in(value) + if _debug: read_property_to_any._debug(" - result: %r", result) + + # return the object + return result + +# +# read_property_to_result_element +# + +@bacpypes_debugging +def read_property_to_result_element(obj, propertyIdentifier, propertyArrayIndex=None): + """Read the specified property of the object, with the optional array index, + and cast the result into an Any object.""" + if _debug: read_property_to_result_element._debug("read_property_to_result_element %s %r %r", obj, propertyIdentifier, propertyArrayIndex) + + # save the result in the property value + read_result = ReadAccessResultElementChoice() + + try: + read_result.propertyValue = read_property_to_any(obj, propertyIdentifier, propertyArrayIndex) + if _debug: read_property_to_result_element._debug(" - success") + except PropertyError as error: + if _debug: read_property_to_result_element._debug(" - error: %r", error) + read_result.propertyAccessError = ErrorType(errorClass='property', errorCode='unknownProperty') + except ExecutionError as error: + if _debug: read_property_to_result_element._debug(" - error: %r", error) + read_result.propertyAccessError = ErrorType(errorClass=error.errorClass, errorCode=error.errorCode) + + # make an element for this value + read_access_result_element = ReadAccessResultElement( + propertyIdentifier=propertyIdentifier, + propertyArrayIndex=propertyArrayIndex, + readResult=read_result, + ) + if _debug: read_property_to_result_element._debug(" - read_access_result_element: %r", read_access_result_element) + + # fini + return read_access_result_element + +# +# ReadWritePropertyMultipleServices +# + +@bacpypes_debugging +class ReadWritePropertyMultipleServices(Capability): + + def __init__(self): + if _debug: ReadWritePropertyMultipleServices._debug("__init__") + Capability.__init__(self) + + def do_ReadPropertyMultipleRequest(self, apdu): + """Respond to a ReadPropertyMultiple Request.""" + if _debug: ReadWritePropertyMultipleServices._debug("do_ReadPropertyMultipleRequest %r", apdu) + + # response is a list of read access results (or an error) + resp = None + read_access_result_list = [] + + # loop through the request + for read_access_spec in apdu.listOfReadAccessSpecs: + # get the object identifier + objectIdentifier = read_access_spec.objectIdentifier + if _debug: ReadWritePropertyMultipleServices._debug(" - objectIdentifier: %r", objectIdentifier) + + # check for wildcard + if (objectIdentifier == ('device', 4194303)) and self.localDevice is not None: + if _debug: ReadWritePropertyMultipleServices._debug(" - wildcard device identifier") + objectIdentifier = self.localDevice.objectIdentifier + + # get the object + obj = self.get_object_id(objectIdentifier) + if _debug: ReadWritePropertyMultipleServices._debug(" - object: %r", obj) + + # make sure it exists + if not obj: + resp = Error(errorClass='object', errorCode='unknownObject', context=apdu) + if _debug: ReadWritePropertyMultipleServices._debug(" - unknown object error: %r", resp) + break + + # build a list of result elements + read_access_result_element_list = [] + + # loop through the property references + for prop_reference in read_access_spec.listOfPropertyReferences: + # get the property identifier + propertyIdentifier = prop_reference.propertyIdentifier + if _debug: ReadWritePropertyMultipleServices._debug(" - propertyIdentifier: %r", propertyIdentifier) + + # get the array index (optional) + propertyArrayIndex = prop_reference.propertyArrayIndex + if _debug: ReadWritePropertyMultipleServices._debug(" - propertyArrayIndex: %r", propertyArrayIndex) + + # check for special property identifiers + if propertyIdentifier in ('all', 'required', 'optional'): + for propId, prop in obj._properties.items(): + if _debug: ReadWritePropertyMultipleServices._debug(" - checking: %r %r", propId, prop.optional) + + if (propertyIdentifier == 'all'): + pass + elif (propertyIdentifier == 'required') and (prop.optional): + if _debug: ReadWritePropertyMultipleServices._debug(" - not a required property") + continue + elif (propertyIdentifier == 'optional') and (not prop.optional): + if _debug: ReadWritePropertyMultipleServices._debug(" - not an optional property") + continue + + # read the specific property + read_access_result_element = read_property_to_result_element(obj, propId, propertyArrayIndex) + + # check for undefined property + if read_access_result_element.readResult.propertyAccessError \ + and read_access_result_element.readResult.propertyAccessError.errorCode == 'unknownProperty': + continue + + # add it to the list + read_access_result_element_list.append(read_access_result_element) + + else: + # read the specific property + read_access_result_element = read_property_to_result_element(obj, propertyIdentifier, propertyArrayIndex) + + # add it to the list + read_access_result_element_list.append(read_access_result_element) + + # build a read access result + read_access_result = ReadAccessResult( + objectIdentifier=objectIdentifier, + listOfResults=read_access_result_element_list + ) + if _debug: ReadWritePropertyMultipleServices._debug(" - read_access_result: %r", read_access_result) + + # add it to the list + read_access_result_list.append(read_access_result) + + # this is a ReadPropertyMultiple ack + if not resp: + resp = ReadPropertyMultipleACK(context=apdu) + resp.listOfReadAccessResults = read_access_result_list + if _debug: ReadWritePropertyMultipleServices._debug(" - resp: %r", resp) + + # return the result + self.response(resp) + +# def do_WritePropertyMultipleRequest(self, apdu): +# """Respond to a WritePropertyMultiple Request.""" +# if _debug: ReadWritePropertyMultipleServices._debug("do_ReadPropertyMultipleRequest %r", apdu) +# +# raise NotImplementedError() diff --git a/py34/bacpypes/service/test.py b/py34/bacpypes/service/test.py new file mode 100644 index 00000000..fb075dda --- /dev/null +++ b/py34/bacpypes/service/test.py @@ -0,0 +1,18 @@ +#!/usr/bin/env python + +""" +Test Service +""" + +from ..debugging import bacpypes_debugging, ModuleLogger + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +@bacpypes_debugging +def some_function(*args): + if _debug: some_function._debug("f %r", args) + + return args[0] + 1 + diff --git a/py34/bacpypes/tcp.py b/py34/bacpypes/tcp.py index 533b15df..226c5857 100755 --- a/py34/bacpypes/tcp.py +++ b/py34/bacpypes/tcp.py @@ -102,68 +102,108 @@ def __init__(self, peer): self.peer = peer # create a request buffer - self.request = '' + self.request = b'' - # hold the socket error if there was one - self.socketError = None + # try to connect + try: + if _debug: TCPClient._debug(" - initiate connection") + self.connect(peer) + except socket.error as err: + if _debug: TCPClient._debug(" - connect socket error: %r", err) - # try to connect the socket - if _debug: TCPClient._debug(" - try to connect") - self.connect(peer) - if _debug: TCPClient._debug(" - connected (maybe)") + # pass along to a handler + self.handle_error(err) def handle_connect(self): - if _debug: deferred(TCPClient._debug, "handle_connect") + if _debug: TCPClient._debug("handle_connect") + + def handle_connect_event(self): + if _debug: TCPClient._debug("handle_connect_event") + + # there might be an error + err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + if _debug: TCPClient._debug(" - err: %r", err) + + # check for connection refused + if (err == 0): + if _debug: TCPClient._debug(" - no error") + elif (err == 111): + if _debug: TCPClient._debug(" - connection to %r refused", self.peer) + self.handle_error(socket.error(111, "connection refused")) + return - def handle_expt(self): - pass + # pass along + asyncore.dispatcher.handle_connect_event(self) def readable(self): - return 1 + return self.connected def handle_read(self): - if _debug: deferred(TCPClient._debug, "handle_read") + if _debug: TCPClient._debug("handle_read") try: msg = self.recv(65536) - if _debug: deferred(TCPClient._debug, " - received %d octets", len(msg)) - self.socketError = None + if _debug: TCPClient._debug(" - received %d octets", len(msg)) # no socket means it was closed if not self.socket: - if _debug: deferred(TCPClient._debug, " - socket was closed") + if _debug: TCPClient._debug(" - socket was closed") else: # sent the data upstream deferred(self.response, PDU(msg)) except socket.error as err: - if (err.args[0] == 111): - deferred(TCPClient._error, "connection to %r refused", self.peer) + if (err.args[0] in (61, 111)): + if _debug: TCPClient._debug(" - connection to %r refused", self.peer) else: - deferred(TCPClient._error, "TCPClient.handle_read socket error: %r", err) - self.socketError = err + if _debug: TCPClient._debug(" - recv socket error: %r", err) + + # pass along to a handler + self.handle_error(err) def writable(self): return (len(self.request) != 0) def handle_write(self): - if _debug: deferred(TCPClient._debug, "handle_write") + if _debug: TCPClient._debug("handle_write") try: sent = self.send(self.request) - if _debug: deferred(TCPClient._debug, " - sent %d octets, %d remaining", sent, len(self.request) - sent) - self.socketError = None + if _debug: TCPClient._debug(" - sent %d octets, %d remaining", sent, len(self.request) - sent) self.request = self.request[sent:] + except socket.error as err: - if (err.args[0] == 111): - deferred(TCPClient._error, "connection to %r refused", self.peer) + if (err.args[0] == 32): + if _debug: TCPClient._debug(" - broken pipe to %r", self.peer) + return + elif (err.args[0] in (61, 111)): + if _debug: TCPClient._debug(" - connection to %r refused", self.peer) else: - deferred(TCPClient._error, "handle_write socket error: %s", err) - self.socketError = err + if _debug: TCPClient._debug(" - send socket error: %s", err) + + # pass along to a handler + self.handle_error(err) + + def handle_write_event(self): + if _debug: TCPClient._debug("handle_write_event") + + # there might be an error + err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + if _debug: TCPClient._debug(" - err: %r", err) + + # check for connection refused + if (err in (61, 111)): + if _debug: TCPClient._debug(" - connection to %r refused", self.peer) + self.handle_error(socket.error(err, "connection refused")) + self.handle_close() + return + + # pass along + asyncore.dispatcher.handle_write_event(self) def handle_close(self): - if _debug: deferred(TCPClient._debug, "handle_close") + if _debug: TCPClient._debug("handle_close") # close the socket self.close() @@ -171,6 +211,13 @@ def handle_close(self): # make sure other routines know the socket is closed self.socket = None + def handle_error(self, error=None): + """Trap for TCPClient errors, otherwise continue.""" + if _debug: TCPClient._debug("handle_error %r", error) + + # core does not take parameters + asyncore.dispatcher.handle_error(self) + def indication(self, pdu): """Requests are queued for delivery.""" if _debug: TCPClient._debug("indication %r", pdu) @@ -208,6 +255,16 @@ def __init__(self, director, peer): # tell the director this is a new actor self.director.add_actor(self) + def handle_error(self, error=None): + """Trap for TCPClient errors, otherwise continue.""" + if _debug: TCPClientActor._debug("handle_error %r", error) + + # pass along to the director + if error is not None: + self.director.actor_error(self, error) + else: + TCPClient.handle_error(self) + def handle_close(self): if _debug: TCPClientActor._debug("handle_close") @@ -220,7 +277,7 @@ def handle_close(self): self.timer.suspend_task() # tell the director this is gone - self.director.remove_actor(self) + self.director.del_actor(self) # pass the function along TCPClient.handle_close(self) @@ -322,23 +379,30 @@ def add_actor(self, actor): # tell the ASE there is a new client if self.serviceElement: - self.sap_request(addPeer=actor.peer) + self.sap_request(add_actor=actor) - def remove_actor(self, actor): + def del_actor(self, actor): """Remove an actor when the socket is closed.""" - if _debug: TCPClientDirector._debug("remove_actor %r", actor) + if _debug: TCPClientDirector._debug("del_actor %r", actor) del self.clients[actor.peer] # tell the ASE the client has gone away if self.serviceElement: - self.sap_request(delPeer=actor.peer) + self.sap_request(del_actor=actor) # see if it should be reconnected if actor.peer in self.reconnect: connect_task = FunctionTask(self.connect, actor.peer) connect_task.install_task(_time() + self.reconnect[actor.peer]) + def actor_error(self, actor, error): + if _debug: TCPClientDirector._debug("actor_error %r %r", actor, error) + + # tell the ASE the actor had an error + if self.serviceElement: + self.sap_request(actor_error=actor, error=error) + def get_actor(self, address): """ Get the actor associated with an address or None. """ return self.clients.get(address, None) @@ -399,70 +463,78 @@ def __init__(self, sock, peer): self.peer = peer # create a request buffer - self.request = '' - - # hold the socket error if there was one - self.socketError = None + self.request = b'' def handle_connect(self): - if _debug: deferred(TCPServer._debug, "handle_connect") + if _debug: TCPServer._debug("handle_connect") def readable(self): return 1 def handle_read(self): - if _debug: deferred(TCPServer._debug, "handle_read") + if _debug: TCPServer._debug("handle_read") try: msg = self.recv(65536) - if _debug: deferred(TCPServer._debug, " - received %d octets", len(msg)) - self.socketError = None + if _debug: TCPServer._debug(" - received %d octets", len(msg)) # no socket means it was closed if not self.socket: - if _debug: deferred(TCPServer._debug, " - socket was closed") + if _debug: TCPServer._debug(" - socket was closed") else: + # send the data upstream deferred(self.response, PDU(msg)) except socket.error as err: if (err.args[0] == 111): - deferred(TCPServer._error, "connection to %r refused", self.peer) + if _debug: TCPServer._debug(" - connection to %r refused", self.peer) else: - deferred(TCPServer._error, "handle_read socket error: %s", err) - self.socketError = err + if _debug: TCPServer._debug(" - recv socket error: %s", err) + + # pass along to a handler + self.handle_error(err) def writable(self): return (len(self.request) != 0) def handle_write(self): - if _debug: deferred(TCPServer._debug, "handle_write") + if _debug: TCPServer._debug("handle_write") try: sent = self.send(self.request) - if _debug: deferred(TCPServer._debug, " - sent %d octets, %d remaining", sent, len(self.request) - sent) - self.socketError = None + if _debug: TCPServer._debug(" - sent %d octets, %d remaining", sent, len(self.request) - sent) self.request = self.request[sent:] - except socket.error as why: - if (why.args[0] == 111): - deferred(TCPServer._error, "connection to %r refused", self.peer) + + except socket.error as err: + if (err.args[0] == 111): + if _debug: TCPServer._debug(" - connection to %r refused", self.peer) else: - deferred(TCPServer._error, "handle_write socket error: %s", why) - self.socketError = why + if _debug: TCPServer._debug(" - send socket error: %s", err) + + # pass along to a handler + self.handle_error(err) def handle_close(self): - if _debug: deferred(TCPServer._debug, "handle_close") + if _debug: TCPServer._debug("handle_close") if not self: - deferred(TCPServer._warning, "handle_close: self is None") + if _debug: TCPServer._warning("handle_close: self is None") return if not self.socket: - deferred(TCPServer._warning, "handle_close: socket already closed") + if _debug: TCPServer._warning("handle_close: socket already closed") return self.close() self.socket = None + def handle_error(self, error=None): + """Trap for TCPServer errors, otherwise continue.""" + if _debug: TCPServer._debug("handle_error %r", error) + + # core does not take parameters + asyncore.dispatcher.handle_error(self) + def indication(self, pdu): """Requests are queued for delivery.""" if _debug: TCPServer._debug("indication %r", pdu) @@ -497,6 +569,16 @@ def __init__(self, director, sock, peer): # tell the director this is a new actor self.director.add_actor(self) + def handle_error(self, error=None): + """Trap for TCPServer errors, otherwise continue.""" + if _debug: TCPServerActor._debug("handle_error %r", error) + + # pass along to the director + if error is not None: + self.director.actor_error(self, error) + else: + TCPServer.handle_error(self) + def handle_close(self): if _debug: TCPServerActor._debug("handle_close") @@ -505,7 +587,7 @@ def handle_close(self): self.flushTask.suspend_task() # tell the director this is gone - self.director.remove_actor(self) + self.director.del_actor(self) # pass it down TCPServer.handle_close(self) @@ -662,19 +744,26 @@ def add_actor(self, actor): # tell the ASE there is a new server if self.serviceElement: - self.sap_request(addPeer=actor.peer) + self.sap_request(add_actor=actor) - def remove_actor(self, actor): - if _debug: TCPServerDirector._debug("remove_actor %r", actor) + def del_actor(self, actor): + if _debug: TCPServerDirector._debug("del_actor %r", actor) try: del self.servers[actor.peer] except KeyError: - TCPServerDirector._warning("remove_actor: %r not an actor", actor) + TCPServerDirector._warning("del_actor: %r not an actor", actor) # tell the ASE the server has gone away if self.serviceElement: - self.sap_request(delPeer=actor.peer) + self.sap_request(del_actor=actor) + + def actor_error(self, actor, error): + if _debug: TCPServerDirector._debug("actor_error %r %r", actor, error) + + # tell the ASE the actor had an error + if self.serviceElement: + self.sap_request(actor_error=actor, error=error) def get_actor(self, address): """ Get the actor associated with an address or None. """ @@ -727,6 +816,7 @@ def chop(addr): # look for a packet while 1: packet = self.packetFn(buff) + if _debug: StreamToPacket._debug(" - packet: %r", packet) if packet is None: break @@ -779,20 +869,23 @@ def __init__(self, stp, aseID=None, sapID=None): # save a reference to the StreamToPacket object self.stp = stp - def indication(self, addPeer=None, delPeer=None): - if _debug: StreamToPacketSAP._debug("indication addPeer=%r delPeer=%r", addPeer, delPeer) + def indication(self, add_actor=None, del_actor=None, actor_error=None, error=None): + if _debug: StreamToPacketSAP._debug("indication add_actor=%r del_actor=%r", add_actor, del_actor) - if addPeer: + if add_actor: # create empty buffers associated with the peer - self.stp.upstreamBuffer[addPeer] = b'' - self.stp.downstreamBuffer[addPeer] = b'' + self.stp.upstreamBuffer[add_actor.peer] = b'' + self.stp.downstreamBuffer[add_actor.peer] = b'' - if delPeer: + if del_actor: # delete the buffer contents associated with the peer - del self.stp.upstreamBuffer[delPeer] - del self.stp.downstreamBuffer[delPeer] + del self.stp.upstreamBuffer[del_actor.peer] + del self.stp.downstreamBuffer[del_actor.peer] # chain this along if self.serviceElement: - self.sap_request(addPeer=addPeer, delPeer=delPeer) - + self.sap_request( + add_actor=add_actor, + del_actor=del_actor, + actor_error=actor_error, error=error, + ) diff --git a/py34/bacpypes/udp.py b/py34/bacpypes/udp.py index 5c1aa1d3..2966efbe 100755 --- a/py34/bacpypes/udp.py +++ b/py34/bacpypes/udp.py @@ -44,19 +44,19 @@ def __init__(self, director, peer): # add a timer self.timeout = director.timeout if self.timeout > 0: - self.timer = FunctionTask(self.IdleTimeout) + self.timer = FunctionTask(self.idle_timeout) self.timer.install_task(_time() + self.timeout) else: self.timer = None # tell the director this is a new actor - self.director.AddActor(self) + self.director.add_actor(self) - def IdleTimeout(self): - if _debug: UDPActor._debug("IdleTimeout") + def idle_timeout(self): + if _debug: UDPActor._debug("idle_timeout") # tell the director this is gone - self.director.RemoveActor(self) + self.director.del_actor(self) def indication(self, pdu): if _debug: UDPActor._debug("indication %r", pdu) @@ -78,6 +78,13 @@ def response(self, pdu): # process this as a response from the director self.director.response(pdu) + def handle_error(self, error=None): + if _debug: UDPActor._debug("handle_error %r", error) + + # pass along to the director + if error is not None: + self.director.actor_error(self, error) + # # UDPPickleActor # @@ -156,52 +163,63 @@ def __init__(self, address, timeout=0, reuse=False, actorClass=UDPActor, sid=Non # start with an empty peer pool self.peers = {} - def AddActor(self, actor): + def add_actor(self, actor): """Add an actor when a new one is connected.""" - if _debug: UDPDirector._debug("AddActor %r", actor) + if _debug: UDPDirector._debug("add_actor %r", actor) self.peers[actor.peer] = actor # tell the ASE there is a new client if self.serviceElement: - self.sap_request(addPeer=actor.peer) + self.sap_request(add_actor=actor) - def RemoveActor(self, actor): + def del_actor(self, actor): """Remove an actor when the socket is closed.""" - if _debug: UDPDirector._debug("RemoveActor %r", actor) + if _debug: UDPDirector._debug("del_actor %r", actor) del self.peers[actor.peer] # tell the ASE the client has gone away if self.serviceElement: - self.sap_request(delPeer=actor.peer) + self.sap_request(del_actor=actor) + + def actor_error(self, actor, error): + if _debug: UDPDirector._debug("actor_error %r %r", actor, error) + + # tell the ASE the actor had an error + if self.serviceElement: + self.sap_request(actor_error=actor, error=error) - def GetActor(self, address): + def get_actor(self, address): return self.peers.get(address, None) def handle_connect(self): - if _debug: deferred(UDPDirector._debug, "handle_connect") + if _debug: UDPDirector._debug("handle_connect") def readable(self): return 1 def handle_read(self): - if _debug: deferred(UDPDirector._debug, "handle_read") + if _debug: UDPDirector._debug("handle_read") try: msg, addr = self.socket.recvfrom(65536) - if _debug: deferred(UDPDirector._debug, " - received %d octets from %s", len(msg), addr) + if _debug: UDPDirector._debug(" - received %d octets from %s", len(msg), addr) # send the PDU up to the client deferred(self._response, PDU(msg, source=addr)) except socket.timeout as err: - deferred(UDPDirector._error, "handle_read socket timeout: %s", err) - except OSError as err: + if _debug: UDPDirector._debug(" - socket timeout: %s", err) + + except socket.error as err: if err.args[0] == 11: pass else: - deferred(UDPDirector._error, "handle_read socket error: %s", err) + if _debug: UDPDirector._debug(" - socket error: %s", err) + + # pass along to a handler + self.handle_error(err) def writable(self): """Return true iff there is a request pending.""" @@ -209,24 +227,36 @@ def writable(self): def handle_write(self): """get a PDU from the queue and send it.""" - if _debug: deferred(UDPDirector._debug, "handle_write") + if _debug: UDPDirector._debug("handle_write") try: pdu = self.request.get() sent = self.socket.sendto(pdu.pduData, pdu.pduDestination) - if _debug: deferred(UDPDirector._debug, " - sent %d octets to %s", sent, pdu.pduDestination) + if _debug: UDPDirector._debug(" - sent %d octets to %s", sent, pdu.pduDestination) - except OSError as err: - deferred(UDPDirector._error, "handle_write socket error: %s", err) + except socket.error as err: + if _debug: UDPDirector._debug(" - socket error: %s", err) + + # get the peer + peer = self.peers.get(pdu.pduDestination, None) + if peer: + # let the actor handle the error + peer.handle_error(err) + else: + # let the director handle the error + self.handle_error(err) def handle_close(self): """Remove this from the monitor when it's closed.""" - if _debug: deferred(UDPDirector._debug, "handle_close") + if _debug: UDPDirector._debug("handle_close") self.close() self.socket = None + def handle_error(self, error=None): + if _debug: UDPDirector._debug("handle_error %r", error) + def indication(self, pdu): """Client requests are queued for delivery.""" if _debug: UDPDirector._debug("indication %r", pdu) diff --git a/samples/AccumulatorObject.py b/samples/AccumulatorObject.py new file mode 100755 index 00000000..a03026ea --- /dev/null +++ b/samples/AccumulatorObject.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python + +""" +This sample application mocks up an accumulator object. +""" + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser + +from bacpypes.core import run +from bacpypes.task import RecurringTask + +from bacpypes.primitivedata import Date, Time +from bacpypes.basetypes import DateTime, Scale +from bacpypes.object import AccumulatorObject + +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject +from bacpypes.service.object import ReadWritePropertyMultipleServices + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# PulseTask +# + +class PulseTask(RecurringTask): + + def __init__(self, accumulator, increment, interval): + if _debug: PulseTask._debug("__init__ %r %r %r", accumulator, increment, interval) + + # this is a recurring task + RecurringTask.__init__(self, interval) + + # install it + self.install_task() + + # save the parameters + self.accumulator = accumulator + self.increment = increment + + def process_task(self): + if _debug: PulseTask._debug("process_task") + + # increment the present value + self.accumulator.presentValue += self.increment + + # update the value change time + current_date = Date().now().value + current_time = Time().now().value + + value_change_time = DateTime(date=current_date, time=current_time) + if _debug: PulseTask._debug(" - value_change_time: %r", value_change_time) + + self.accumulator.valueChangeTime = value_change_time + +bacpypes_debugging(PulseTask) + +# +# __main__ +# + +def main(): + # parse the command line arguments + args = ConfigArgumentParser(description=__doc__).parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # make a device object + this_device = LocalDeviceObject( + objectName=args.ini.objectname, + objectIdentifier=('device', int(args.ini.objectidentifier)), + maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), + segmentationSupported=args.ini.segmentationsupported, + vendorIdentifier=int(args.ini.vendoridentifier), + ) + + # make a sample application + this_application = BIPSimpleApplication(this_device, args.ini.address) + + # add the additional service + this_application.add_capability(ReadWritePropertyMultipleServices) + + # 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 + + # make a random input object + accumulator = AccumulatorObject( + objectIdentifier=('accumulator', 1), + objectName='Something1', + presentValue=100, + statusFlags = [0, 0, 0, 0], + eventState='normal', + scale=Scale(floatScale=2.3), + units='btusPerPoundDryAir', + ) + if _debug: _log.debug(" - accumulator: %r", accumulator) + + # add it to the device + this_application.add_object(accumulator) + if _debug: _log.debug(" - object list: %r", this_device.objectList) + + # create a task that bumps the value by one every 10 seconds + pulse_task = PulseTask(accumulator, 1, 10 * 1000) + if _debug: _log.debug(" - pulse_task: %r", pulse_task) + + _log.debug("running") + + run() + + _log.debug("fini") + + +if __name__ == "__main__": + main() diff --git a/samples/BBMD.py b/samples/BBMD.py new file mode 100755 index 00000000..6e2d1bcf --- /dev/null +++ b/samples/BBMD.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python + +""" +This sample application presents itself as a BBMD sitting on an IP network. +The first parameter is the address of the BBMD itself and the second and +subsequent parameters are the entries to put in its broadcast distribution +table. +""" + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ArgumentParser + +from bacpypes.core import run +from bacpypes.comm import Client, bind + +from bacpypes.pdu import Address +from bacpypes.bvllservice import BIPBBMD, AnnexJCodec, UDPMultiplexer + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + + +# +# NullClient +# + +@bacpypes_debugging +class NullClient(Client): + + def __init__(self, cid=None): + if _debug: NullClient._debug("__init__ cid=%r", cid) + Client.__init__(self, cid=cid) + + def confirmation(self, *args, **kwargs): + if _debug: NullClient._debug("confirmation %r %r", args, kwargs) + +# +# __main__ +# + +def main(): + # parse the command line arguments + parser = ArgumentParser(description=__doc__) + + # add an argument for interval + parser.add_argument('localaddr', type=str, + help='local address of the BBMD', + ) + + # add an argument for interval + parser.add_argument('bdtentry', type=str, nargs='*', + help='list of addresses of peers', + ) + + # now parse the arguments + args = parser.parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + local_address = Address(args.localaddr) + if _debug: _log.debug(" - local_address: %r", local_address) + + # create a null client that will accept, but do nothing with upstream + # packets from the BBMD + null_client = NullClient() + if _debug: _log.debug(" - null_client: %r", null_client) + + # create a BBMD, bound to the Annex J server on a UDP multiplexer + bbmd = BIPBBMD(local_address) + annexj = AnnexJCodec() + multiplexer = UDPMultiplexer(local_address) + + # bind the layers together + bind(null_client, bbmd, annexj, multiplexer.annexJ) + + # loop through the rest of the addresses + for bdtentry in args.bdtentry: + if _debug: _log.debug(" - bdtentry: %r", bdtentry) + + bdt_address = Address(bdtentry) + bbmd.add_peer(bdt_address) + + if _debug: _log.debug(" - bbmd: %r", bbmd) + + _log.debug("running") + + run() + + _log.debug("fini") + + +if __name__ == "__main__": + main() diff --git a/samples/BBMD2VLANRouter.py b/samples/BBMD2VLANRouter.py index bb8c0349..980ecf60 100755 --- a/samples/BBMD2VLANRouter.py +++ b/samples/BBMD2VLANRouter.py @@ -28,8 +28,10 @@ from bacpypes.netservice import NetworkServiceAccessPoint, NetworkServiceElement from bacpypes.bvllservice import BIPBBMD, AnnexJCodec, UDPMultiplexer -from bacpypes.app import LocalDeviceObject, Application +from bacpypes.app import Application from bacpypes.appservice import StateMachineAccessPoint, ApplicationServiceAccessPoint +from bacpypes.service.device import LocalDeviceObject, WhoIsIAmServices +from bacpypes.service.object import ReadWritePropertyServices from bacpypes.primitivedata import Real from bacpypes.object import AnalogValueObject, Property @@ -89,7 +91,7 @@ def __init__(self, **kwargs): # @bacpypes_debugging -class VLANApplication(Application): +class VLANApplication(Application, WhoIsIAmServices, ReadWritePropertyServices): def __init__(self, vlan_device, vlan_address, aseID=None): if _debug: VLANApplication._debug("__init__ %r %r aseID=%r", vlan_device, vlan_address, aseID) @@ -102,6 +104,10 @@ def __init__(self, vlan_device, vlan_address, aseID=None): # can know if it should support segmentation self.smap = StateMachineAccessPoint(vlan_device) + # the segmentation state machines need access to the same device + # information cache as the application + self.smap.deviceInfoCache = self.deviceInfoCache + # a network service access point will be needed self.nsap = NetworkServiceAccessPoint() @@ -151,7 +157,7 @@ def __init__(self, local_address, local_network): self.nse = NetworkServiceElement() bind(self.nse, self.nsap) - # create a BBMD, bound to the Annex J server + # create a BBMD, bound to the Annex J server # on the UDP multiplexer self.bip = BIPBBMD(local_address) self.annexj = AnnexJCodec() diff --git a/samples/SubscribeCOV.py b/samples/COVClient.py similarity index 74% rename from samples/SubscribeCOV.py rename to samples/COVClient.py index be8e9dde..29281fa4 100755 --- a/samples/SubscribeCOV.py +++ b/samples/COVClient.py @@ -1,9 +1,10 @@ #!/usr/bin/env python """ -This application presents a 'console' prompt to the user asking for read commands -which create ReadPropertyRequest PDUs, then lines up the coorresponding ReadPropertyACK -and prints the value. +This application presents a 'console' prompt to the user asking for +subscribe commands which create SubscribeCOVRequests. The other commands are +for changing the type of reply to the confirmed COV notification that gets +sent. """ from bacpypes.debugging import bacpypes_debugging, ModuleLogger @@ -11,14 +12,16 @@ from bacpypes.consolecmd import ConsoleCmd from bacpypes.core import run, enable_sleeping +from bacpypes.iocb import IOCB from bacpypes.pdu import Address -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication from bacpypes.object import get_object_class - from bacpypes.apdu import SubscribeCOVRequest, \ SimpleAckPDU, RejectPDU, AbortPDU +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -40,34 +43,18 @@ def __init__(self, *args): if _debug: SubscribeCOVApplication._debug("__init__ %r", args) BIPSimpleApplication.__init__(self, *args) - # keep track of requests to line up responses - self._request = None - - def request(self, apdu): - if _debug: SubscribeCOVApplication._debug("request %r", apdu) - - # save a copy of the request - self._request = apdu - - # forward it along - BIPSimpleApplication.request(self, apdu) - - def confirmation(self, apdu): - if _debug: SubscribeCOVApplication._debug("confirmation %r", apdu) - - # continue normally - super(SubscribeCOVApplication, self).confirmation(apdu) - - def indication(self, apdu): - if _debug: SubscribeCOVApplication._debug("indication %r", apdu) - - # continue normally - super(SubscribeCOVApplication, self).indication(apdu) - def do_ConfirmedCOVNotificationRequest(self, apdu): if _debug: SubscribeCOVApplication._debug("do_ConfirmedCOVNotificationRequest %r", apdu) global rsvp + print("{} changed\n {}".format( + apdu.monitoredObjectIdentifier, + ",\n ".join("{} = {}".format( + element.propertyIdentifier, + str(element.value), + ) for element in apdu.listOfValues), + )) + if rsvp[0]: # success response = SimpleAckPDU(context=apdu) @@ -89,6 +76,13 @@ def do_ConfirmedCOVNotificationRequest(self, apdu): def do_UnconfirmedCOVNotificationRequest(self, apdu): if _debug: SubscribeCOVApplication._debug("do_UnconfirmedCOVNotificationRequest %r", apdu) + print("{} changed\n {}".format( + apdu.monitoredObjectIdentifier, + ",\n ".join("{} is {}".format( + element.propertyIdentifier, + str(element.value), + ) for element in apdu.listOfValues), + )) # # SubscribeCOVConsoleCmd @@ -99,6 +93,8 @@ class SubscribeCOVConsoleCmd(ConsoleCmd): def do_subscribe(self, args): """subscribe addr proc_id obj_type obj_inst [ confirmed ] [ lifetime ] + + Generate a SubscribeCOVRequest and wait for the response. """ args = args.split() if _debug: SubscribeCOVConsoleCmd._debug("do_subscribe %r", args) @@ -149,14 +145,32 @@ def do_subscribe(self, args): if _debug: SubscribeCOVConsoleCmd._debug(" - request: %r", request) + # make an IOCB + iocb = IOCB(request) + if _debug: SubscribeCOVConsoleCmd._debug(" - iocb: %r", iocb) + # give it to the application - this_application.request(request) + this_application.request_io(iocb) + + # wait for it to complete + iocb.wait() + + # do something for success + if iocb.ioResponse: + if _debug: SubscribeCOVConsoleCmd._debug(" - response: %r", iocb.ioResponse) + + # do something for error/reject/abort + if iocb.ioError: + if _debug: SubscribeCOVConsoleCmd._debug(" - error: %r", iocb.ioError) except Exception as e: SubscribeCOVConsoleCmd._exception("exception: %r", e) def do_ack(self, args): """ack + + When confirmed COV notification requests arrive, respond with a + simple acknowledgement. """ args = args.split() if _debug: SubscribeCOVConsoleCmd._debug("do_ack %r", args) @@ -166,18 +180,24 @@ def do_ack(self, args): def do_reject(self, args): """reject reason + + When confirmed COV notification requests arrive, respond with a + reject PDU with the provided reason. """ args = args.split() - if _debug: SubscribeCOVConsoleCmd._debug("do_subscribe %r", args) + if _debug: SubscribeCOVConsoleCmd._debug("do_reject %r", args) global rsvp rsvp = (False, args[0], None) def do_abort(self, args): """abort reason + + When confirmed COV notification requests arrive, respond with an + abort PDU with the provided reason. """ args = args.split() - if _debug: SubscribeCOVConsoleCmd._debug("do_subscribe %r", args) + if _debug: SubscribeCOVConsoleCmd._debug("do_abort %r", args) global rsvp rsvp = (False, None, args[0]) diff --git a/samples/COVMixin.py b/samples/COVMixin.py deleted file mode 100755 index 280b2f9f..00000000 --- a/samples/COVMixin.py +++ /dev/null @@ -1,1176 +0,0 @@ -#!/usr/bin/env python - -""" -This sample application shows how to extend the basic functionality of a device -to support the ReadPropertyMultiple service. -""" - -from collections import defaultdict - -from bacpypes.debugging import bacpypes_debugging, DebugContents, ModuleLogger -from bacpypes.consolelogging import ConfigArgumentParser -from bacpypes.consolecmd import ConsoleCmd -from bacpypes.errors import ExecutionError - -from bacpypes.core import run, enable_sleeping -from bacpypes.task import OneShotTask, TaskManager -from bacpypes.pdu import Address - -from bacpypes.constructeddata import SequenceOf, Any -from bacpypes.basetypes import DeviceAddress, COVSubscription, PropertyValue, \ - Recipient, RecipientProcess, ObjectPropertyReference -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication -from bacpypes.object import Object, Property, PropertyError, \ - get_object_class, register_object_type, \ - AccessDoorObject, AccessPointObject, \ - AnalogInputObject, AnalogOutputObject, AnalogValueObject, \ - LargeAnalogValueObject, IntegerValueObject, PositiveIntegerValueObject, \ - LightingOutputObject, BinaryInputObject, BinaryOutputObject, \ - BinaryValueObject, LifeSafetyPointObject, LifeSafetyZoneObject, \ - MultiStateInputObject, MultiStateOutputObject, MultiStateValueObject, \ - OctetStringValueObject, CharacterStringValueObject, TimeValueObject, \ - DateTimeValueObject, DateValueObject, TimePatternValueObject, \ - DatePatternValueObject, DateTimePatternValueObject, \ - CredentialDataInputObject, LoadControlObject, LoopObject, \ - PulseConverterObject -from bacpypes.apdu import SubscribeCOVRequest, \ - ConfirmedCOVNotificationRequest, \ - UnconfirmedCOVNotificationRequest, \ - SimpleAckPDU, Error, RejectPDU, AbortPDU - -# some debugging -_debug = 0 -_log = ModuleLogger(globals()) - -# globals -_generic_criteria_classes = {} -_cov_increment_criteria_classes = {} - -# test globals -test_application = None - -# -# SubscriptionList -# - -@bacpypes_debugging -class SubscriptionList: - - def __init__(self): - if _debug: SubscriptionList._debug("__init__") - - self.cov_subscriptions = [] - - def append(self, cov): - if _debug: SubscriptionList._debug("append %r", cov) - - self.cov_subscriptions.append(cov) - - def remove(self, cov): - if _debug: SubscriptionList._debug("remove %r", cov) - - self.cov_subscriptions.remove(cov) - - def find(self, client_addr, proc_id, obj_id): - if _debug: SubscriptionList._debug("find %r %r %r", client_addr, proc_id, obj_id) - - for cov in self.cov_subscriptions: - all_equal = (cov.client_addr == client_addr) and \ - (cov.proc_id == proc_id) and \ - (cov.obj_id == obj_id) - if _debug: SubscriptionList._debug(" - cov, all_equal: %r %r", cov, all_equal) - - if all_equal: - return cov - - return None - - def __len__(self): - if _debug: SubscriptionList._debug("__len__") - - return len(self.cov_subscriptions) - - def __iter__(self): - if _debug: SubscriptionList._debug("__iter__") - - for cov in self.cov_subscriptions: - yield cov - - -# -# Subscription -# - -@bacpypes_debugging -class Subscription(OneShotTask, DebugContents): - - _debug_contents = ( - 'obj_ref', - 'client_addr', - 'proc_id', - 'obj_id', - 'confirmed', - 'lifetime', - ) - - def __init__(self, obj_ref, client_addr, proc_id, obj_id, confirmed, lifetime): - if _debug: Subscription._debug("__init__ %r %r %r %r %r %r", obj_ref, client_addr, proc_id, obj_id, confirmed, lifetime) - OneShotTask.__init__(self) - - # save the reference to the related object - self.obj_ref = obj_ref - - # save the parameters - self.client_addr = client_addr - self.proc_id = proc_id - self.obj_id = obj_id - self.confirmed = confirmed - self.lifetime = lifetime - - # add ourselves to the subscription list for this object - obj_ref._cov_subscriptions.append(self) - - # add ourselves to the list of all active subscriptions - obj_ref._app.active_cov_subscriptions.append(self) - - # if lifetime is non-zero, schedule the subscription to expire - if lifetime != 0: - self.install_task(delta=self.lifetime) - - def cancel_subscription(self): - if _debug: Subscription._debug("cancel_subscription") - - # suspend the task - self.suspend_task() - - # remove ourselves from the other subscriptions for this object - self.obj_ref._cov_subscriptions.remove(self) - - # remove ourselves from the list of all active subscriptions - self.obj_ref._app.active_cov_subscriptions.remove(self) - - # break the object reference - self.obj_ref = None - - def renew_subscription(self, lifetime): - if _debug: Subscription._debug("renew_subscription") - - # suspend iff scheduled - if self.isScheduled: - self.suspend_task() - - # reschedule the task if its not infinite - if lifetime != 0: - self.install_task(delta=lifetime) - - def process_task(self): - if _debug: Subscription._debug("process_task") - - # subscription is canceled - self.cancel_subscription() - -# -# COVCriteria -# - -@bacpypes_debugging -class COVCriteria: - - _properties_tracked = () - _properties_reported = () - _monitored_property_reference = None - - def _check_criteria(self): - if _debug: COVCriteria._debug("_check_criteria") - - # assume nothing has changed - something_changed = False - - # check all the things - for property_name in self._properties_tracked: - property_changed = (self._values[property_name] != self._cov_properties[property_name]) - if property_changed: - if _debug: COVCriteria._debug(" - %s changed", property_name) - - # copy the new value for next time - self._cov_properties[property_name] = self._values[property_name] - - something_changed = True - - if not something_changed: - if _debug: COVCriteria._debug(" - nothing changed") - - # should send notifications - return something_changed - - -@bacpypes_debugging -class GenericCriteria(COVCriteria): - - _properties_tracked = ( - 'presentValue', - 'statusFlags', - ) - _properties_reported = ( - 'presentValue', - 'statusFlags', - ) - _monitored_property_reference = 'presentValue' - - -@bacpypes_debugging -class COVIncrementCriteria(COVCriteria): - - _properties_tracked = ( - 'presentValue', - 'statusFlags', - ) - _properties_reported = ( - 'presentValue', - 'statusFlags', - ) - _monitored_property_reference = 'presentValue' - - def _check_criteria(self): - if _debug: COVIncrementCriteria._debug("_check_criteria") - - # assume nothing has changed - something_changed = False - - # get the old and new values - old_present_value = self._cov_properties['presentValue'] - new_present_value = self._values['presentValue'] - cov_increment = self._values['covIncrement'] - - # check the difference in values - value_changed = (new_present_value <= (old_present_value - cov_increment)) \ - or (new_present_value >= (old_present_value + cov_increment)) - if value_changed: - if _debug: COVIncrementCriteria._debug(" - present value changed") - - # copy the new value for next time - self._cov_properties['presentValue'] = new_present_value - - something_changed = True - - # check the status flags - status_changed = (self._values['statusFlags'] != self._cov_properties['statusFlags']) - if status_changed: - if _debug: COVIncrementCriteria._debug(" - status flags changed") - - # copy the new value for next time - self._cov_properties['statusFlags'] = self._values['statusFlags'] - - something_changed = True - - if not something_changed: - if _debug: COVIncrementCriteria._debug(" - nothing changed") - - # should send notifications - return something_changed - -# -# Change of Value Mixin -# - -@bacpypes_debugging -class COVObjectMixin(object): - - _debug_contents = ( - '_cov_subscriptions', - '_cov_properties', - ) - - def __init__(self, **kwargs): - if _debug: COVObjectMixin._debug("__init__ %r", kwargs) - super(COVObjectMixin, self).__init__(**kwargs) - - # list of all active subscriptions - self._cov_subscriptions = SubscriptionList() - - # snapshot the properties tracked - self._cov_properties = {} - for property_name in self._properties_tracked: - self._cov_properties[property_name] = self._values[property_name] - - def __setattr__(self, attr, value): - if _debug: COVObjectMixin._debug("__setattr__ %r %r", attr, value) - - if attr.startswith('_') or attr[0].isupper() or (attr == 'debug_contents'): - return object.__setattr__(self, attr, value) - - # use the default implementation - super(COVObjectMixin, self).__setattr__(attr, value) - - # check for special properties - if attr in self._properties_tracked: - if _debug: COVObjectMixin._debug(" - property tracked") - - # check if it is significant - if self._check_criteria(): - if _debug: COVObjectMixin._debug(" - send notifications") - self._send_cov_notifications() - else: - if _debug: COVObjectMixin._debug(" - no notifications necessary") - else: - if _debug: COVObjectMixin._debug(" - property not tracked") - - def WriteProperty(self, propid, value, arrayIndex=None, priority=None, direct=False): - if _debug: COVObjectMixin._debug("WriteProperty %r %r arrayIndex=%r priority=%r", propid, value, arrayIndex, priority) - - # normalize the property identifier - if isinstance(propid, int): - # get the property - prop = self._properties.get(propid) - if _debug: Object._debug(" - prop: %r", prop) - - if not prop: - raise PropertyError(propid) - - # use the name from now on - propid = prop.identifier - if _debug: Object._debug(" - propid: %r", propid) - - # use the default implementation - super(COVObjectMixin, self).WriteProperty(propid, value, arrayIndex, priority, direct) - - # check for special properties - if propid in self._properties_tracked: - if _debug: COVObjectMixin._debug(" - property tracked") - - # check if it is significant - if self._check_criteria(): - if _debug: COVObjectMixin._debug(" - send notifications") - self._send_cov_notifications() - else: - if _debug: COVObjectMixin._debug(" - no notifications necessary") - else: - if _debug: COVObjectMixin._debug(" - property not tracked") - - def _send_cov_notifications(self): - if _debug: COVObjectMixin._debug("_send_cov_notifications") - - # check for subscriptions - if not len(self._cov_subscriptions): - return - - # get the current time from the task manager - current_time = TaskManager().get_time() - if _debug: COVObjectMixin._debug(" - current_time: %r", current_time) - - # create a list of values - list_of_values = [] - for property_name in self._properties_reported: - if _debug: COVObjectMixin._debug(" - property_name: %r", property_name) - - # get the class - property_datatype = self.get_datatype(property_name) - if _debug: COVObjectMixin._debug(" - property_datatype: %r", property_datatype) - - # build the value - bundle_value = property_datatype(self._values[property_name]) - if _debug: COVObjectMixin._debug(" - bundle_value: %r", bundle_value) - - # bundle it into a sequence - property_value = PropertyValue( - propertyIdentifier=property_name, - value=Any(bundle_value), - ) - - # add it to the list - list_of_values.append(property_value) - if _debug: COVObjectMixin._debug(" - list_of_values: %r", list_of_values) - - # loop through the subscriptions and send out notifications - for cov in self._cov_subscriptions: - if _debug: COVObjectMixin._debug(" - cov: %r", cov) - - # calculate time remaining - if not cov.lifetime: - time_remaining = 0 - else: - time_remaining = int(cov.taskTime - current_time) - - # make sure it is at least one second - if not time_remaining: - time_remaining = 1 - - # build a request with the correct type - if cov.confirmed: - request = ConfirmedCOVNotificationRequest() - else: - request = UnconfirmedCOVNotificationRequest() - - # fill in the parameters - request.pduDestination = cov.client_addr - request.subscriberProcessIdentifier = cov.proc_id - request.initiatingDeviceIdentifier = self._app.localDevice.objectIdentifier - request.monitoredObjectIdentifier = cov.obj_id - request.timeRemaining = time_remaining - request.listOfValues = list_of_values - if _debug: COVObjectMixin._debug(" - request: %r", request) - - # let the application send it - self._app.cov_notification(cov, request) - -# --------------------------- -# access door -# --------------------------- - -@bacpypes_debugging -class AccessDoorCriteria(COVCriteria): - - _properties_tracked = ( - 'presentValue', - 'statusFlags', - 'doorAlarmState', - ) - _properties_reported = ( - 'presentValue', - 'statusFlags', - 'doorAlarmState', - ) - -@register_object_type -class AccessDoorObjectCOV(COVObjectMixin, AccessDoorCriteria, AccessDoorObject): - pass - -# --------------------------- -# access point -# --------------------------- - -@bacpypes_debugging -class AccessPointCriteria(COVCriteria): - - _properties_tracked = ( - 'accessEventTime', - 'statusFlags', - ) - _properties_reported = ( - 'accessEvent', - 'statusFlags', - 'accessEventTag', - 'accessEventTime', - 'accessEventCredential', - 'accessEventAuthenticationFactor', - ) - _monitored_property_reference = 'accessEvent' - -@register_object_type -class AccessPointObjectCOV(COVObjectMixin, AccessPointCriteria, AccessPointObject): - pass - -# --------------------------- -# analog objects -# --------------------------- - -@register_object_type -class AnalogInputObjectCOV(COVObjectMixin, COVIncrementCriteria, AnalogInputObject): - pass - -@register_object_type -class AnalogOutputObjectCOV(COVObjectMixin, COVIncrementCriteria, AnalogOutputObject): - pass - -@register_object_type -class AnalogValueObjectCOV(COVObjectMixin, COVIncrementCriteria, AnalogValueObject): - pass - -@register_object_type -class LargeAnalogValueObjectCOV(COVObjectMixin, COVIncrementCriteria, LargeAnalogValueObject): - pass - -@register_object_type -class IntegerValueObjectCOV(COVObjectMixin, COVIncrementCriteria, IntegerValueObject): - pass - -@register_object_type -class PositiveIntegerValueObjectCOV(COVObjectMixin, COVIncrementCriteria, PositiveIntegerValueObject): - pass - -@register_object_type -class LightingOutputObjectCOV(COVObjectMixin, COVIncrementCriteria, LightingOutputObject): - pass - -# --------------------------- -# generic objects -# --------------------------- - -@register_object_type -class BinaryInputObjectCOV(COVObjectMixin, GenericCriteria, BinaryInputObject): - pass - -@register_object_type -class BinaryOutputObjectCOV(COVObjectMixin, GenericCriteria, BinaryOutputObject): - pass - -@register_object_type -class BinaryValueObjectCOV(COVObjectMixin, GenericCriteria, BinaryValueObject): - pass - -@register_object_type -class LifeSafetyPointObjectCOV(COVObjectMixin, GenericCriteria, LifeSafetyPointObject): - pass - -@register_object_type -class LifeSafetyZoneObjectCOV(COVObjectMixin, GenericCriteria, LifeSafetyZoneObject): - pass - -@register_object_type -class MultiStateInputObjectCOV(COVObjectMixin, GenericCriteria, MultiStateInputObject): - pass - -@register_object_type -class MultiStateOutputObjectCOV(COVObjectMixin, GenericCriteria, MultiStateOutputObject): - pass - -@register_object_type -class MultiStateValueObjectCOV(COVObjectMixin, GenericCriteria, MultiStateValueObject): - pass - -@register_object_type -class OctetStringValueObjectCOV(COVObjectMixin, GenericCriteria, OctetStringValueObject): - pass - -@register_object_type -class CharacterStringValueObjectCOV(COVObjectMixin, GenericCriteria, CharacterStringValueObject): - pass - -@register_object_type -class TimeValueObjectCOV(COVObjectMixin, GenericCriteria, TimeValueObject): - pass - -@register_object_type -class DateTimeValueObjectCOV(COVObjectMixin, GenericCriteria, DateTimeValueObject): - pass - -@register_object_type -class DateValueObjectCOV(COVObjectMixin, GenericCriteria, DateValueObject): - pass - -@register_object_type -class TimePatternValueObjectCOV(COVObjectMixin, GenericCriteria, TimePatternValueObject): - pass - -@register_object_type -class DatePatternValueObjectCOV(COVObjectMixin, GenericCriteria, DatePatternValueObject): - pass - -@register_object_type -class DateTimePatternValueObjectCOV(COVObjectMixin, GenericCriteria, DateTimePatternValueObject): - pass - -# --------------------------- -# credential data input -# --------------------------- - -@bacpypes_debugging -class CredentialDataInputCriteria(COVCriteria): - - _properties_tracked = ( - 'updateTime', - 'statusFlags' - ) - _properties_reported = ( - 'presentValue', - 'statusFlags', - 'updateTime', - ) - -@register_object_type -class CredentialDataInputObjectCOV(COVObjectMixin, CredentialDataInputCriteria, CredentialDataInputObject): - pass - -# --------------------------- -# load control -# --------------------------- - -@bacpypes_debugging -class LoadControlCriteria(COVCriteria): - - _properties_tracked = ( - 'presentValue', - 'statusFlags', - 'requestedShedLevel', - 'startTime', - 'shedDuration', - 'dutyWindow', - ) - _properties_reported = ( - 'presentValue', - 'statusFlags', - 'requestedShedLevel', - 'startTime', - 'shedDuration', - 'dutyWindow', - ) - -@register_object_type -class LoadControlObjectCOV(COVObjectMixin, LoadControlCriteria, LoadControlObject): - pass - -# --------------------------- -# loop -# --------------------------- - -@register_object_type -class LoopObjectCOV(COVObjectMixin, COVIncrementCriteria, LoopObject): - pass - -# --------------------------- -# pulse converter -# --------------------------- - -@bacpypes_debugging -class PulseConverterCriteria(): - - _properties_tracked = ( - 'presentValue', - 'statusFlags', - ) - _properties_reported = ( - 'presentValue', - 'statusFlags', - ) - -@register_object_type -class PulseConverterObjectCOV(COVObjectMixin, PulseConverterCriteria, PulseConverterObject): - pass - -# -# COVApplicationMixin -# - -@bacpypes_debugging -class COVApplicationMixin(object): - - def __init__(self, *args, **kwargs): - if _debug: COVApplicationMixin._debug("__init__ %r %r", args, kwargs) - super(COVApplicationMixin, self).__init__(*args, **kwargs) - - # list of active subscriptions - self.active_cov_subscriptions = [] - - # a queue of confirmed notifications by client address - self.confirmed_notifications_queue = defaultdict(list) - - def cov_notification(self, cov, request): - if _debug: COVApplicationMixin._debug("cov_notification %s %s", str(cov), str(request)) - - # if this is confirmed, keep track of the cov - if cov.confirmed: - if _debug: COVApplicationMixin._debug(" - it's confirmed") - - notification_list = self.confirmed_notifications_queue[cov.client_addr] - notification_list.append((request, cov)) - - # if this isn't the first, wait until the first one is done - if len(notification_list) > 1: - if _debug: COVApplicationMixin._debug(" - not the first") - return - else: - if _debug: COVApplicationMixin._debug(" - it's unconfirmed") - - # send it along down the stack - super(COVApplicationMixin, self).request(request) - if _debug: COVApplicationMixin._debug(" - apduInvokeID: %r", getattr(request, 'apduInvokeID')) - - def cov_error(self, cov, request, response): - if _debug: COVApplicationMixin._debug("cov_error %r %r %r", cov, request, response) - - def cov_reject(self, cov, request, response): - if _debug: COVApplicationMixin._debug("cov_reject %r %r %r", cov, request, response) - - def cov_abort(self, cov, request, response): - if _debug: COVApplicationMixin._debug("cov_abort %r %r %r", cov, request, response) - - # delete the rest of the pending requests for this client - del self.confirmed_notifications_queue[cov.client_addr][:] - if _debug: COVApplicationMixin._debug(" - other notifications deleted") - - def confirmation(self, apdu): - if _debug: COVApplicationMixin._debug("confirmation %r", apdu) - - if _debug: COVApplicationMixin._debug(" - queue keys: %r", self.confirmed_notifications_queue.keys()) - - # if this isn't from someone we care about, toss it - if apdu.pduSource not in self.confirmed_notifications_queue: - if _debug: COVApplicationMixin._debug(" - not someone we are tracking") - - # pass along to the application - super(COVApplicationMixin, self).confirmation(apdu) - return - - # refer to the notification list for this client - notification_list = self.confirmed_notifications_queue[apdu.pduSource] - if _debug: COVApplicationMixin._debug(" - notification_list: %r", notification_list) - - # peek at the front of the list - request, cov = notification_list[0] - if _debug: COVApplicationMixin._debug(" - request: %s", request) - - # line up the invoke id - if apdu.apduInvokeID == request.apduInvokeID: - if _debug: COVApplicationMixin._debug(" - request/response align") - notification_list.pop(0) - else: - if _debug: COVApplicationMixin._debug(" - request/response do not align") - - # pass along to the application - super(COVApplicationMixin, self).confirmation(apdu) - return - - if isinstance(apdu, Error): - if _debug: COVApplicationMixin._debug(" - error: %r", apdu.errorCode) - self.cov_error(cov, request, apdu) - - elif isinstance(apdu, RejectPDU): - if _debug: COVApplicationMixin._debug(" - reject: %r", apdu.apduAbortRejectReason) - self.cov_reject(cov, request, apdu) - - elif isinstance(apdu, AbortPDU): - if _debug: COVApplicationMixin._debug(" - abort: %r", apdu.apduAbortRejectReason) - self.cov_abort(cov, request, apdu) - - # if the notification list is empty, delete the reference - if not notification_list: - if _debug: COVApplicationMixin._debug(" - no other pending notifications") - del self.confirmed_notifications_queue[apdu.pduSource] - return - - # peek at the front of the list for the next request - request, cov = notification_list[0] - if _debug: COVApplicationMixin._debug(" - next notification: %r", request) - - # send it along down the stack - super(COVApplicationMixin, self).request(request) - - def do_SubscribeCOVRequest(self, apdu): - if _debug: COVApplicationMixin._debug("do_SubscribeCOVRequest %r", apdu) - - # extract the pieces - client_addr = apdu.pduSource - proc_id = apdu.subscriberProcessIdentifier - obj_id = apdu.monitoredObjectIdentifier - confirmed = apdu.issueConfirmedNotifications - lifetime = apdu.lifetime - - # request is to cancel the subscription - cancel_subscription = (confirmed is None) and (lifetime is None) - - # find the object - obj = self.get_object_id(obj_id) - if not obj: - if _debug: COVConsoleCmd._debug(" - object not found") - self.response(Error(errorClass='object', errorCode='unknownObject', context=apdu)) - return - - # can a match be found? - cov = obj._cov_subscriptions.find(client_addr, proc_id, obj_id) - if _debug: COVConsoleCmd._debug(" - cov: %r", cov) - - # if a match was found, update the subscription - if cov: - if cancel_subscription: - if _debug: COVConsoleCmd._debug(" - cancel the subscription") - cov.cancel_subscription() - else: - if _debug: COVConsoleCmd._debug(" - renew the subscription") - cov.renew_subscription(lifetime) - else: - if cancel_subscription: - if _debug: COVConsoleCmd._debug(" - cancel a subscription that doesn't exist") - else: - if _debug: COVConsoleCmd._debug(" - create a subscription") - - cov = Subscription(obj, client_addr, proc_id, obj_id, confirmed, lifetime) - if _debug: COVConsoleCmd._debug(" - cov: %r", cov) - - # success - response = SimpleAckPDU(context=apdu) - - # return the result - self.response(response) - -# -# ActiveCOVSubscriptions -# - -@bacpypes_debugging -class ActiveCOVSubscriptions(Property): - - def __init__(self, identifier): - Property.__init__( - self, identifier, SequenceOf(COVSubscription), - default=None, optional=True, mutable=False, - ) - - def ReadProperty(self, obj, arrayIndex=None): - if _debug: ActiveCOVSubscriptions._debug("ReadProperty %s arrayIndex=%r", obj, arrayIndex) - - # get the current time from the task manager - current_time = TaskManager().get_time() - if _debug: ActiveCOVSubscriptions._debug(" - current_time: %r", current_time) - - # start with an empty sequence - cov_subscriptions = SequenceOf(COVSubscription)() - - # the obj is a DeviceObject with a reference to the application - for cov in obj._app.active_cov_subscriptions: - # calculate time remaining - if not cov.lifetime: - time_remaining = 0 - else: - time_remaining = int(cov.taskTime - current_time) - - # make sure it is at least one second - if not time_remaining: - time_remaining = 1 - - recipient_process = RecipientProcess( - recipient=Recipient( - address=DeviceAddress( - networkNumber=cov.client_addr.addrNet or 0, - macAddress=cov.client_addr.addrAddr, - ), - ), - processIdentifier=cov.proc_id, - ) - - cov_subscription = COVSubscription( - recipient=recipient_process, - monitoredPropertyReference=ObjectPropertyReference( - objectIdentifier=cov.obj_id, - propertyIdentifier=cov.obj_ref._monitored_property_reference, - ), - issueConfirmedNotifications=cov.confirmed, - timeRemaining=time_remaining, - # covIncrement=???, - ) - if _debug: ActiveCOVSubscriptions._debug(" - cov_subscription: %r", cov_subscription) - - # add the list - cov_subscriptions.append(cov_subscription) - - return cov_subscriptions - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None): - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# COVDeviceObject -# - -@bacpypes_debugging -class COVDeviceMixin(object): - - properties = [ - ActiveCOVSubscriptions('activeCovSubscriptions'), - ] - -class LocalDeviceObjectCOV(COVDeviceMixin, LocalDeviceObject): - pass - -# -# SubscribeCOVApplication -# - -@bacpypes_debugging -class SubscribeCOVApplication(COVApplicationMixin, BIPSimpleApplication): - pass - -# -# COVConsoleCmd -# - -@bacpypes_debugging -class COVConsoleCmd(ConsoleCmd): - - def do_subscribe(self, args): - """subscribe addr proc_id obj_type obj_inst [ confirmed ] [ lifetime ] - """ - args = args.split() - if _debug: COVConsoleCmd._debug("do_subscribe %r", args) - global test_application - - try: - addr, proc_id, obj_type, obj_inst = args[:4] - - client_addr = Address(addr) - if _debug: COVConsoleCmd._debug(" - client_addr: %r", client_addr) - - proc_id = int(proc_id) - if _debug: COVConsoleCmd._debug(" - proc_id: %r", proc_id) - - if obj_type.isdigit(): - obj_type = int(obj_type) - elif not get_object_class(obj_type): - raise ValueError("unknown object type") - obj_inst = int(obj_inst) - obj_id = (obj_type, obj_inst) - if _debug: COVConsoleCmd._debug(" - obj_id: %r", obj_id) - - obj = test_application.get_object_id(obj_id) - if not obj: - print("object not found") - return - - if len(args) >= 5: - issue_confirmed = args[4] - if issue_confirmed == '-': - issue_confirmed = None - else: - issue_confirmed = issue_confirmed.lower() == 'true' - if _debug: COVConsoleCmd._debug(" - issue_confirmed: %r", issue_confirmed) - else: - issue_confirmed = None - - if len(args) >= 6: - lifetime = args[5] - if lifetime == '-': - lifetime = None - else: - lifetime = int(lifetime) - if _debug: COVConsoleCmd._debug(" - lifetime: %r", lifetime) - else: - lifetime = None - - # can a match be found? - cov = obj._cov_subscriptions.find(client_addr, proc_id, obj_id) - if _debug: COVConsoleCmd._debug(" - cov: %r", cov) - - # build a request - request = SubscribeCOVRequest( - subscriberProcessIdentifier=proc_id, - monitoredObjectIdentifier=obj_id, - ) - - # spoof that it came from the client - request.pduSource = client_addr - - # optional parameters - if issue_confirmed is not None: - request.issueConfirmedNotifications = issue_confirmed - if lifetime is not None: - request.lifetime = lifetime - - if _debug: COVConsoleCmd._debug(" - request: %r", request) - - # give it to the application - test_application.do_SubscribeCOVRequest(request) - - except Exception as err: - COVConsoleCmd._exception("exception: %r", err) - - def do_status(self, args): - """status [ object_name ]""" - args = args.split() - if _debug: COVConsoleCmd._debug("do_status %r", args) - global test_application - - if args: - obj = test_application.get_object_name(args[0]) - if not obj: - print("no such object") - else: - print("%s %s" % (obj.objectName, obj.objectIdentifier)) - obj.debug_contents() - else: - # dump the information about all the known objects - for obj in test_application.iter_objects(): - print("%s %s" % (obj.objectName, obj.objectIdentifier)) - obj.debug_contents() - - def do_trigger(self, args): - """trigger object_name""" - args = args.split() - if _debug: COVConsoleCmd._debug("do_trigger %r", args) - global test_application - - if not args: - print("object name required") - else: - obj = test_application.get_object_name(args[0]) - if not obj: - print("no such object") - else: - obj._send_cov_notifications() - - def do_set(self, args): - """set object_name [ . ] property_name [ = ] value""" - args = args.split() - if _debug: COVConsoleCmd._debug("do_set %r", args) - global test_application - - try: - object_name = args.pop(0) - if '.' in object_name: - object_name, property_name = object_name.split('.') - else: - property_name = args.pop(0) - if _debug: COVConsoleCmd._debug(" - object_name: %r", object_name) - if _debug: COVConsoleCmd._debug(" - property_name: %r", property_name) - - obj = test_application.get_object_name(object_name) - if _debug: COVConsoleCmd._debug(" - obj: %r", obj) - if not obj: - raise RuntimeError("object not found: %r" % (object_name,)) - - datatype = obj.get_datatype(property_name) - if _debug: COVConsoleCmd._debug(" - datatype: %r", datatype) - if not datatype: - raise RuntimeError("not a property: %r" % (property_name,)) - - # toss the equals - if args[0] == '=': - args.pop(0) - - # evaluate the value - value = eval(args.pop(0)) - if _debug: COVConsoleCmd._debug(" - raw value: %r", value) - - # see if it can be built - obj_value = datatype(value) - if _debug: COVConsoleCmd._debug(" - obj_value: %r", obj_value) - - # normalize - value = obj_value.value - if _debug: COVConsoleCmd._debug(" - normalized value: %r", value) - - # change the value - setattr(obj, property_name, value) - - except IndexError: - print(COVConsoleCmd.do_set.__doc__) - except Exception as err: - print("exception: %s" % (err,)) - - def do_write(self, args): - """write object_name [ . ] property [ = ] value""" - args = args.split() - if _debug: COVConsoleCmd._debug("do_set %r", args) - global test_application - - try: - object_name = args.pop(0) - if '.' in object_name: - object_name, property_name = object_name.split('.') - else: - property_name = args.pop(0) - if _debug: COVConsoleCmd._debug(" - object_name: %r", object_name) - if _debug: COVConsoleCmd._debug(" - property_name: %r", property_name) - - obj = test_application.get_object_name(object_name) - if _debug: COVConsoleCmd._debug(" - obj: %r", obj) - if not obj: - raise RuntimeError("object not found: %r" % (object_name,)) - - datatype = obj.get_datatype(property_name) - if _debug: COVConsoleCmd._debug(" - datatype: %r", datatype) - if not datatype: - raise RuntimeError("not a property: %r" % (property_name,)) - - # toss the equals - if args[0] == '=': - args.pop(0) - - # evaluate the value - value = eval(args.pop(0)) - if _debug: COVConsoleCmd._debug(" - raw value: %r", value) - - # see if it can be built - obj_value = datatype(value) - if _debug: COVConsoleCmd._debug(" - obj_value: %r", obj_value) - - # normalize - value = obj_value.value - if _debug: COVConsoleCmd._debug(" - normalized value: %r", value) - - # pass it along - obj.WriteProperty(property_name, value) - - except IndexError: - print(COVConsoleCmd.do_write.__doc__) - except Exception as err: - print("exception: %s" % (err,)) - - -def main(): - global test_application - - # make a parser - parser = ConfigArgumentParser(description=__doc__) - parser.add_argument("--console", - action="store_true", - default=False, - help="create a console", - ) - - # parse the command line arguments - args = parser.parse_args() - - if _debug: _log.debug("initialization") - if _debug: _log.debug(" - args: %r", args) - - # make a device object - test_device = LocalDeviceObjectCOV( - objectName=args.ini.objectname, - objectIdentifier=int(args.ini.objectidentifier), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # make a sample application - test_application = SubscribeCOVApplication(test_device, args.ini.address) - - # make a binary value object - test_bvo = BinaryValueObjectCOV( - objectIdentifier=('binaryValue', 1), - objectName='bvo', - presentValue='inactive', - statusFlags=[0, 0, 0, 0], - ) - _log.debug(" - test_bvo: %r", test_bvo) - - # add it to the device - test_application.add_object(test_bvo) - - # make an analog value object - test_avo = AnalogValueObjectCOV( - objectIdentifier=('analogValue', 1), - objectName='avo', - presentValue=0.0, - statusFlags=[0, 0, 0, 0], - covIncrement=1.0, - ) - _log.debug(" - test_avo: %r", test_avo) - - # add it to the device - test_application.add_object(test_avo) - _log.debug(" - object list: %r", test_device.objectList) - - # get the services supported - services_supported = test_application.get_services_supported() - if _debug: _log.debug(" - services_supported: %r", services_supported) - - # let the device object know - test_device.protocolServicesSupported = services_supported.value - - # make a console - if args.console: - test_console = COVConsoleCmd() - _log.debug(" - test_console: %r", test_console) - - # enable sleeping will help with threads - enable_sleeping() - - _log.debug("running") - - run() - - _log.debug("fini") - - -if __name__ == "__main__": - main() diff --git a/samples/COVServer.py b/samples/COVServer.py new file mode 100755 index 00000000..1c909bf6 --- /dev/null +++ b/samples/COVServer.py @@ -0,0 +1,271 @@ +#!/usr/bin/env python + +""" +This sample application is a server that supports COV notification services. +The console accepts commands that change the properties of an object that +triggers the notifications. +""" + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser +from bacpypes.consolecmd import ConsoleCmd + +from bacpypes.core import run, enable_sleeping + +from bacpypes.app import BIPSimpleApplication +from bacpypes.object import AnalogValueObject, BinaryValueObject +from bacpypes.service.device import LocalDeviceObject +from bacpypes.service.cov import ChangeOfValueServices + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# test globals +test_application = None + +# +# SubscribeCOVApplication +# + +@bacpypes_debugging +class SubscribeCOVApplication(BIPSimpleApplication, ChangeOfValueServices): + pass + +# +# COVConsoleCmd +# + +@bacpypes_debugging +class COVConsoleCmd(ConsoleCmd): + + def do_status(self, args): + """status""" + args = args.split() + if _debug: COVConsoleCmd._debug("do_status %r", args) + global test_application + + # reference the list of active subscriptions + active_subscriptions = test_application.active_cov_subscriptions + if _debug: COVConsoleCmd._debug(" - %d active subscriptions", len(active_subscriptions)) + + # dump then out + for subscription in active_subscriptions: + print("{} {} {} {} {}".format( + subscription.client_addr, + subscription.proc_id, + subscription.obj_id, + subscription.confirmed, + subscription.lifetime, + )) + + # reference the object to algorithm map + object_detections = test_application.object_detections + for objref, cov_detection in object_detections.items(): + print("{} {}".format( + objref.objectIdentifier, + cov_detection, + )) + + def do_trigger(self, args): + """trigger object_name""" + args = args.split() + if _debug: COVConsoleCmd._debug("do_trigger %r", args) + global test_application + + if not args: + print("object name required") + return + + obj = test_application.get_object_name(args[0]) + if not obj: + print("no such object") + return + + # get the detection algorithm object + cov_detection = test_application.cov_detections.get(obj, None) + if (not cov_detection) or (len(cov_detection.cov_subscriptions) == 0): + print("no subscriptions for that object") + return + + # tell it to send out notifications + cov_detection.send_cov_notifications() + + def do_set(self, args): + """set object_name [ . ] property_name [ = ] value""" + args = args.split() + if _debug: COVConsoleCmd._debug("do_set %r", args) + global test_application + + try: + object_name = args.pop(0) + if '.' in object_name: + object_name, property_name = object_name.split('.') + else: + property_name = args.pop(0) + if _debug: COVConsoleCmd._debug(" - object_name: %r", object_name) + if _debug: COVConsoleCmd._debug(" - property_name: %r", property_name) + + obj = test_application.get_object_name(object_name) + if _debug: COVConsoleCmd._debug(" - obj: %r", obj) + if not obj: + raise RuntimeError("object not found: %r" % (object_name,)) + + datatype = obj.get_datatype(property_name) + if _debug: COVConsoleCmd._debug(" - datatype: %r", datatype) + if not datatype: + raise RuntimeError("not a property: %r" % (property_name,)) + + # toss the equals + if args[0] == '=': + args.pop(0) + + # evaluate the value + value = eval(args.pop(0)) + if _debug: COVConsoleCmd._debug(" - raw value: %r", value) + + # see if it can be built + obj_value = datatype(value) + if _debug: COVConsoleCmd._debug(" - obj_value: %r", obj_value) + + # normalize + value = obj_value.value + if _debug: COVConsoleCmd._debug(" - normalized value: %r", value) + + # change the value + setattr(obj, property_name, value) + + except IndexError: + print(COVConsoleCmd.do_set.__doc__) + except Exception as err: + print("exception: %s" % (err,)) + + def do_write(self, args): + """write object_name [ . ] property [ = ] value""" + args = args.split() + if _debug: COVConsoleCmd._debug("do_set %r", args) + global test_application + + try: + object_name = args.pop(0) + if '.' in object_name: + object_name, property_name = object_name.split('.') + else: + property_name = args.pop(0) + if _debug: COVConsoleCmd._debug(" - object_name: %r", object_name) + if _debug: COVConsoleCmd._debug(" - property_name: %r", property_name) + + obj = test_application.get_object_name(object_name) + if _debug: COVConsoleCmd._debug(" - obj: %r", obj) + if not obj: + raise RuntimeError("object not found: %r" % (object_name,)) + + datatype = obj.get_datatype(property_name) + if _debug: COVConsoleCmd._debug(" - datatype: %r", datatype) + if not datatype: + raise RuntimeError("not a property: %r" % (property_name,)) + + # toss the equals + if args[0] == '=': + args.pop(0) + + # evaluate the value + value = eval(args.pop(0)) + if _debug: COVConsoleCmd._debug(" - raw value: %r", value) + + # see if it can be built + obj_value = datatype(value) + if _debug: COVConsoleCmd._debug(" - obj_value: %r", obj_value) + + # normalize + value = obj_value.value + if _debug: COVConsoleCmd._debug(" - normalized value: %r", value) + + # pass it along + obj.WriteProperty(property_name, value) + + except IndexError: + print(COVConsoleCmd.do_write.__doc__) + except Exception as err: + print("exception: %s" % (err,)) + + +def main(): + global test_application + + # make a parser + parser = ConfigArgumentParser(description=__doc__) + parser.add_argument("--console", + action="store_true", + default=False, + help="create a console", + ) + + # parse the command line arguments + args = parser.parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # make a device object + test_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), + ) + + # make a sample application + test_application = SubscribeCOVApplication(test_device, args.ini.address) + + # make a binary value object + test_bv = BinaryValueObject( + objectIdentifier=('binaryValue', 1), + objectName='bv', + presentValue='inactive', + statusFlags=[0, 0, 0, 0], + ) + _log.debug(" - test_bv: %r", test_bv) + + # add it to the device + test_application.add_object(test_bv) + + # make an analog value object + test_av = AnalogValueObject( + objectIdentifier=('analogValue', 1), + objectName='av', + presentValue=0.0, + statusFlags=[0, 0, 0, 0], + covIncrement=1.0, + ) + _log.debug(" - test_av: %r", test_av) + + # add it to the device + test_application.add_object(test_av) + _log.debug(" - object list: %r", test_device.objectList) + + # get the services supported + services_supported = test_application.get_services_supported() + if _debug: _log.debug(" - services_supported: %r", services_supported) + + # let the device object know + test_device.protocolServicesSupported = services_supported.value + + # make a console + if args.console: + test_console = COVConsoleCmd() + _log.debug(" - test_console: %r", test_console) + + # enable sleeping will help with threads + enable_sleeping() + + _log.debug("running") + + run() + + _log.debug("fini") + + +if __name__ == "__main__": + main() diff --git a/samples/CommandableMixin.py b/samples/CommandableMixin.py index f58a2901..2bce0862 100755 --- a/samples/CommandableMixin.py +++ b/samples/CommandableMixin.py @@ -12,11 +12,13 @@ from bacpypes.core import run from bacpypes.errors import ExecutionError -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication from bacpypes.object import AnalogValueObject, DateValueObject -from bacpypes.primitivedata import Null +from bacpypes.primitivedata import Null, Date from bacpypes.basetypes import PriorityValue, PriorityArray +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -146,7 +148,7 @@ class CommandableDateValueObject(CommandableMixin, DateValueObject): def __init__(self, **kwargs): if _debug: CommandableDateValueObject._debug("__init__ %r", kwargs) - CommandableMixin.__init__(self, False, **kwargs) + CommandableMixin.__init__(self, None, **kwargs) # # __main__ @@ -173,14 +175,18 @@ def main(): # make a commandable analog value object, add to the device cavo1 = CommandableAnalogValueObject( - objectIdentifier=('analogValue', 1), objectName='Commandable AV 1' + objectIdentifier=('analogValue', 1), objectName='Commandable1', ) if _debug: _log.debug(" - cavo1: %r", cavo1) this_application.add_object(cavo1) - # make a commandable binary value object, add to the device + # get the current date + today = Date().now() + + # make a commandable date value object, add to the device cdvo2 = CommandableDateValueObject( - objectIdentifier=('dateValue', 1), objectName='Commandable2' + objectIdentifier=('dateValue', 1), objectName='Commandable2', + presentValue=today.value, ) if _debug: _log.debug(" - cdvo2: %r", cdvo2) this_application.add_object(cdvo2) diff --git a/samples/SampleApplication.py b/samples/HandsOnLab/Sample1_SimpleApplication.py old mode 100755 new mode 100644 similarity index 87% rename from samples/SampleApplication.py rename to samples/HandsOnLab/Sample1_SimpleApplication.py index a7771939..b96478b7 --- a/samples/SampleApplication.py +++ b/samples/HandsOnLab/Sample1_SimpleApplication.py @@ -1,17 +1,10 @@ #!/usr/bin/env python """ -Sample 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. - -There is no input or output for this application, but by adding --debug to -the command line when it is run you can check the behavior of the stack by -seeing what is sent and received. """ from bacpypes.debugging import bacpypes_debugging, ModuleLogger @@ -19,10 +12,11 @@ from bacpypes.core import run -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject # some debugging -_debug = 0 +_debug = 1 _log = ModuleLogger(globals()) # @@ -58,7 +52,7 @@ def confirmation(self, apdu): # def main(): - # parse the command line arguments + # parse the command line arguments and initialize loggers args = ConfigArgumentParser(description=__doc__).parse_args() if _debug: _log.debug("initialization") @@ -71,7 +65,7 @@ def main(): maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), segmentationSupported=args.ini.segmentationsupported, vendorIdentifier=int(args.ini.vendoridentifier), - vendorName="Hello", + vendorName="B612", ) # make a sample application diff --git a/samples/WhoIsIAmApplication.py b/samples/HandsOnLab/Sample2_WhoIsIAmApplication.py old mode 100755 new mode 100644 similarity index 92% rename from samples/WhoIsIAmApplication.py rename to samples/HandsOnLab/Sample2_WhoIsIAmApplication.py index 73d06d5a..27d00499 --- a/samples/WhoIsIAmApplication.py +++ b/samples/HandsOnLab/Sample2_WhoIsIAmApplication.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -This sample application builds on the first sample by overriding the default +This sample application builds on the first sample by overriding the default processing for Who-Is and I-Am requests, counting them, then continuing on with the regular processing. After the run() function has completed it will dump a formatted summary of the requests it has received. @@ -14,7 +14,8 @@ from bacpypes.core import run -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject # some debugging _debug = 0 @@ -48,7 +49,7 @@ def do_WhoIsRequest(self, apdu): # count the times this has been received who_is_counter[key] += 1 - # pass back to the default implementation + # continue with the default implementation BIPSimpleApplication.do_WhoIsRequest(self, apdu) def do_IAmRequest(self, apdu): @@ -63,7 +64,8 @@ def do_IAmRequest(self, apdu): # count the times this has been received i_am_counter[key] += 1 - # no default implementation + # continue with the default implementation + BIPSimpleApplication.do_IAmRequest(self, apdu) # # __main__ diff --git a/samples/WhoHasIHaveApplication.py b/samples/HandsOnLab/Sample3_WhoHasIHaveApplication.py old mode 100755 new mode 100644 similarity index 96% rename from samples/WhoHasIHaveApplication.py rename to samples/HandsOnLab/Sample3_WhoHasIHaveApplication.py index 65a90f2b..9c06c0b2 --- a/samples/WhoHasIHaveApplication.py +++ b/samples/HandsOnLab/Sample3_WhoHasIHaveApplication.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -This sample application builds on the first sample by overriding the default +This sample application builds on the first sample by overriding the default processing for Who-Has and I-Have requests, counting them, then continuing on with the regular processing. After the run() function has completed it will dump a formatted summary of the requests it has received. Note that these @@ -15,7 +15,8 @@ from bacpypes.core import run -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject # some debugging _debug = 0 diff --git a/samples/RandomAnalogValueObject.py b/samples/HandsOnLab/Sample4_RandomAnalogValueObject.py old mode 100755 new mode 100644 similarity index 95% rename from samples/RandomAnalogValueObject.py rename to samples/HandsOnLab/Sample4_RandomAnalogValueObject.py index d12b8e8b..8aeb5a44 --- a/samples/RandomAnalogValueObject.py +++ b/samples/HandsOnLab/Sample4_RandomAnalogValueObject.py @@ -17,10 +17,12 @@ from bacpypes.core import run from bacpypes.primitivedata import Real -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication from bacpypes.object import AnalogValueObject, Property, register_object_type from bacpypes.errors import ExecutionError +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -32,7 +34,6 @@ # RandomValueProperty # -@bacpypes_debugging class RandomValueProperty(Property): def __init__(self, identifier): @@ -56,11 +57,12 @@ def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False if _debug: RandomValueProperty._debug("WriteProperty %r %r arrayIndex=%r priority=%r direct=%r", obj, value, arrayIndex, priority, direct) raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') +bacpypes_debugging(RandomValueProperty) + # # Random Value Object Type # -@bacpypes_debugging class RandomAnalogValueObject(AnalogValueObject): properties = [ @@ -71,6 +73,7 @@ def __init__(self, **kwargs): if _debug: RandomAnalogValueObject._debug("__init__ %r", kwargs) AnalogValueObject.__init__(self, **kwargs) +bacpypes_debugging(RandomAnalogValueObject) register_object_type(RandomAnalogValueObject) # diff --git a/samples/IP2IPRouter.py b/samples/IP2IPRouter.py index 8145027b..3d83f491 100755 --- a/samples/IP2IPRouter.py +++ b/samples/IP2IPRouter.py @@ -3,9 +3,9 @@ """ This sample application presents itself as a router between two IP networks. This application can run on a single homed machine -by using the same IP address and two different port numbers, or -to be closer to what is typically considered a router, on a -multihomed machine using two different IP addresses and the same +by using the same IP address and two different port numbers, or +to be closer to what is typically considered a router, on a +multihomed machine using two different IP addresses and the same port number. $ python IP2IPRtouer.py addr1 net1 addr2 net2 @@ -51,7 +51,7 @@ def __init__(self, addr1, net1, addr2, net2): #== First stack - # create a generic BIP stack, bound to the Annex J server + # create a generic BIP stack, bound to the Annex J server # on the UDP multiplexer self.s1_bip = BIPSimple() self.s1_annexj = AnnexJCodec() @@ -65,7 +65,7 @@ def __init__(self, addr1, net1, addr2, net2): #== Second stack - # create a generic BIP stack, bound to the Annex J server + # create a generic BIP stack, bound to the Annex J server # on the UDP multiplexer self.s2_bip = BIPSimple() self.s2_annexj = AnnexJCodec() diff --git a/samples/MultiStateValueObject.py b/samples/MultiStateValueObject.py index 873de076..6465728c 100755 --- a/samples/MultiStateValueObject.py +++ b/samples/MultiStateValueObject.py @@ -10,9 +10,13 @@ from bacpypes.core import run -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication +from bacpypes.primitivedata import CharacterString +from bacpypes.constructeddata import ArrayOf from bacpypes.object import MultiStateValueObject +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -49,11 +53,11 @@ def main(): # make a multistate value object msvo = MultiStateValueObject( - objectIdentifier=('multiStateValue', 1), + objectIdentifier=('multiStateValue', 1), objectName='My Special Object', presentValue=1, numberOfStates=3, - stateText=['red', 'green', 'blue'], + stateText=ArrayOf(CharacterString)(['red', 'green', 'blue']), ) _log.debug(" - msvo: %r", msvo) diff --git a/samples/MultipleReadProperty.py b/samples/MultipleReadProperty.py index 6e483afd..e7bce7f8 100755 --- a/samples/MultipleReadProperty.py +++ b/samples/MultipleReadProperty.py @@ -3,7 +3,7 @@ """ Mutliple Read Property -This application has a static list of points that it would like to read. It reads the +This application has a static list of points that it would like to read. It reads the values of each of them in turn and then quits. """ @@ -13,23 +13,29 @@ from bacpypes.consolelogging import ConfigArgumentParser from bacpypes.core import run, stop, deferred +from bacpypes.iocb import IOCB from bacpypes.pdu import Address -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication from bacpypes.object import get_datatype from bacpypes.apdu import ReadPropertyRequest, Error, AbortPDU, ReadPropertyACK from bacpypes.primitivedata import Unsigned from bacpypes.constructeddata import Array +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject + # some debugging _debug = 0 _log = ModuleLogger(globals()) +# globals +this_application = None + # point list, set according to your device point_list = [ - ('1.2.3.4', 'analogValue', 1, 'presentValue'), - ('1.2.3.4', 'analogValue', 2, 'presentValue'), + ('10.0.1.14', 'analogValue', 1, 'presentValue'), + ('10.0.1.14', 'analogValue', 2, 'presentValue'), ] # @@ -43,15 +49,12 @@ def __init__(self, point_list, *args): if _debug: ReadPointListApplication._debug("__init__ %r, %r", point_list, args) BIPSimpleApplication.__init__(self, *args) - # keep track of requests to line up responses - self._request = None + # turn the point list into a queue + self.point_queue = deque(point_list) # make a list of the response values self.response_values = [] - # turn the point list into a queue - self.point_queue = deque(point_list) - def next_request(self): if _debug: ReadPointListApplication._debug("next_request") @@ -65,28 +68,29 @@ def next_request(self): addr, obj_type, obj_inst, prop_id = self.point_queue.popleft() # build a request - self._request = ReadPropertyRequest( + request = ReadPropertyRequest( objectIdentifier=(obj_type, obj_inst), propertyIdentifier=prop_id, ) - self._request.pduDestination = Address(addr) - if _debug: ReadPointListApplication._debug(" - request: %r", self._request) + request.pduDestination = Address(addr) + if _debug: ReadPointListApplication._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) - # forward it along - BIPSimpleApplication.request(self, self._request) + # set a callback for the response + iocb.add_callback(self.complete_request) + if _debug: ReadPointListApplication._debug(" - iocb: %r", iocb) - def confirmation(self, apdu): - if _debug: ReadPointListApplication._debug("confirmation %r", apdu) + # send the request + this_application.request_io(iocb) - if isinstance(apdu, Error): - if _debug: ReadPointListApplication._debug(" - error: %r", apdu) - self.response_values.append(apdu) + def complete_request(self, iocb): + if _debug: ReadPointListApplication._debug("complete_request %r", iocb) - elif isinstance(apdu, AbortPDU): - if _debug: ReadPointListApplication._debug(" - abort: %r", apdu) - self.response_values.append(apdu) + if iocb.ioResponse: + apdu = iocb.ioResponse - elif (isinstance(self._request, ReadPropertyRequest)) and (isinstance(apdu, ReadPropertyACK)): # find the datatype datatype = get_datatype(apdu.objectIdentifier[0], apdu.propertyIdentifier) if _debug: ReadPointListApplication._debug(" - datatype: %r", datatype) @@ -106,6 +110,10 @@ def confirmation(self, apdu): # save the value self.response_values.append(value) + if iocb.ioError: + if _debug: ReadPointListApplication._debug(" - error: %r", iocb.ioError) + self.response_values.append(iocb.ioError) + # fire off another request deferred(self.next_request) @@ -114,6 +122,8 @@ def confirmation(self, apdu): # def main(): + global this_application + # parse the command line arguments args = ConfigArgumentParser(description=__doc__).parse_args() diff --git a/samples/MultipleReadPropertyThreaded.py b/samples/MultipleReadPropertyThreaded.py new file mode 100755 index 00000000..9620b8ed --- /dev/null +++ b/samples/MultipleReadPropertyThreaded.py @@ -0,0 +1,163 @@ +#!/usr/bin/env python + +""" +Mutliple Read Property + +This application has a static list of points that it would like to read. It reads the +values of each of them in turn and then quits. +""" + +from collections import deque +from threading import Thread + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser + +from bacpypes.core import run, stop, deferred +from bacpypes.iocb import IOCB + +from bacpypes.pdu import Address +from bacpypes.object import get_datatype + +from bacpypes.apdu import ReadPropertyRequest +from bacpypes.primitivedata import Unsigned +from bacpypes.constructeddata import Array + +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +this_application = None + +# point list, set according to your device +point_list = [ + ('10.0.1.14', 'analogValue', 1, 'presentValue'), + ('10.0.1.14', 'analogValue', 2, 'presentValue'), + ] + +# +# ReadPointListThread +# + +@bacpypes_debugging +class ReadPointListThread(Thread): + + def __init__(self, point_list): + if _debug: ReadPointListThread._debug("__init__ %r", point_list) + Thread.__init__(self) + + # turn the point list into a queue + self.point_queue = deque(point_list) + + # make a list of the response values + self.response_values = [] + + def run(self): + if _debug: ReadPointListThread._debug("run") + global this_application + + # loop through the points + for addr, obj_type, obj_inst, prop_id in self.point_queue: + # build a request + request = ReadPropertyRequest( + objectIdentifier=(obj_type, obj_inst), + propertyIdentifier=prop_id, + ) + request.pduDestination = Address(addr) + if _debug: ReadPointListThread._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) + if _debug: ReadPointListThread._debug(" - iocb: %r", iocb) + + # give it to the application + this_application.request_io(iocb) + + # wait for the response + iocb.wait() + + if iocb.ioResponse: + apdu = iocb.ioResponse + + # find the datatype + datatype = get_datatype(apdu.objectIdentifier[0], apdu.propertyIdentifier) + if _debug: ReadPointListThread._debug(" - datatype: %r", datatype) + if not datatype: + raise TypeError("unknown datatype") + + # special case for array parts, others are managed by cast_out + if issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): + if apdu.propertyArrayIndex == 0: + value = apdu.propertyValue.cast_out(Unsigned) + else: + value = apdu.propertyValue.cast_out(datatype.subtype) + else: + value = apdu.propertyValue.cast_out(datatype) + if _debug: ReadPointListThread._debug(" - value: %r", value) + + # save the value + self.response_values.append(value) + + if iocb.ioError: + if _debug: ReadPointListThread._debug(" - error: %r", iocb.ioError) + self.response_values.append(iocb.ioError) + + # done + stop() + +# +# __main__ +# + +def main(): + global this_application + + # parse the command line arguments + args = ConfigArgumentParser(description=__doc__).parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # 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), + ) + + # make a simple application + this_application = BIPSimpleApplication(this_device, args.ini.address) + + # 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 + + # create a thread and + read_thread = ReadPointListThread(point_list) + if _debug: _log.debug(" - read_thread: %r", read_thread) + + # start it running when the core is running + deferred(read_thread.start) + + _log.debug("running") + + run() + + # dump out the results + for request, response in zip(point_list, read_thread.response_values): + print(request, response) + + _log.debug("fini") + + +if __name__ == "__main__": + main() diff --git a/samples/RandomAccumulatorObject.py b/samples/RandomAccumulatorObject.py deleted file mode 100755 index 59d4c63b..00000000 --- a/samples/RandomAccumulatorObject.py +++ /dev/null @@ -1,149 +0,0 @@ -#!/usr/bin/env python - -""" -This sample application mocks up an accumulator object. -""" - -import random - -from bacpypes.debugging import bacpypes_debugging, ModuleLogger -from bacpypes.consolelogging import ConfigArgumentParser - -from bacpypes.core import run - -from bacpypes.primitivedata import Unsigned, Date, Time -from bacpypes.basetypes import DateTime -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication -from bacpypes.object import AccumulatorObject, Property, register_object_type -from bacpypes.errors import ExecutionError - -# some debugging -_debug = 0 -_log = ModuleLogger(globals()) - -# -# RandomUnsignedValueProperty -# - -@bacpypes_debugging -class RandomUnsignedValueProperty(Property): - - def __init__(self, identifier): - if _debug: RandomUnsignedValueProperty._debug("__init__ %r", identifier) - Property.__init__(self, identifier, Unsigned, default=None, optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - if _debug: RandomUnsignedValueProperty._debug("ReadProperty %r arrayIndex=%r", obj, arrayIndex) - - # access an array - if arrayIndex is not None: - raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') - - # return a random value - value = int(random.random() * 100.0) - if _debug: RandomUnsignedValueProperty._debug(" - value: %r", value) - - return value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - if _debug: RandomUnsignedValueProperty._debug("WriteProperty %r %r arrayIndex=%r priority=%r direct=%r", obj, value, arrayIndex, priority, direct) - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# CurrentDateTimeProperty -# - -@bacpypes_debugging -class CurrentDateTimeProperty(Property): - - def __init__(self, identifier): - if _debug: CurrentDateTimeProperty._debug("__init__ %r", identifier) - Property.__init__(self, identifier, DateTime, default=None, optional=True, mutable=False) - - def ReadProperty(self, obj, arrayIndex=None): - if _debug: CurrentDateTimeProperty._debug("ReadProperty %r arrayIndex=%r", obj, arrayIndex) - - # access an array - if arrayIndex is not None: - raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') - - # get the value - current_date = Date().now().value - current_time = Time().now().value - - value = DateTime(date=current_date, time=current_time) - if _debug: CurrentDateTimeProperty._debug(" - value: %r", value) - - return value - - def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): - if _debug: CurrentDateTimeProperty._debug("WriteProperty %r %r arrayIndex=%r priority=%r direct=%r", obj, value, arrayIndex, priority, direct) - raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') - -# -# Random Accumulator Object -# - -@bacpypes_debugging -class RandomAccumulatorObject(AccumulatorObject): - - properties = [ - RandomUnsignedValueProperty('presentValue'), - CurrentDateTimeProperty('valueChangeTime'), - ] - - def __init__(self, **kwargs): - if _debug: RandomAccumulatorObject._debug("__init__ %r", kwargs) - AccumulatorObject.__init__(self, **kwargs) - -register_object_type(RandomAccumulatorObject) - -# -# __main__ -# - -def main(): - # parse the command line arguments - args = ConfigArgumentParser(description=__doc__).parse_args() - - if _debug: _log.debug("initialization") - if _debug: _log.debug(" - args: %r", args) - - # make a device object - this_device = LocalDeviceObject( - objectName=args.ini.objectname, - objectIdentifier=('device', int(args.ini.objectidentifier)), - maxApduLengthAccepted=int(args.ini.maxapdulengthaccepted), - segmentationSupported=args.ini.segmentationsupported, - vendorIdentifier=int(args.ini.vendoridentifier), - ) - - # make a sample application - this_application = BIPSimpleApplication(this_device, args.ini.address) - - # 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 - - # make a random input object - rao1 = RandomAccumulatorObject( - objectIdentifier=('accumulator', 1), - objectName='Random1', - statusFlags = [0, 0, 0, 0], - ) - _log.debug(" - rao1: %r", rao1) - - # add it to the device - this_application.add_object(rao1) - _log.debug(" - object list: %r", this_device.objectList) - - run() - - _log.debug("fini") - - -if __name__ == "__main__": - main() diff --git a/samples/ReadProperty.py b/samples/ReadProperty.py index 7fe3a537..da42e7ee 100755 --- a/samples/ReadProperty.py +++ b/samples/ReadProperty.py @@ -13,77 +13,23 @@ from bacpypes.consolecmd import ConsoleCmd from bacpypes.core import run, enable_sleeping +from bacpypes.iocb import IOCB from bacpypes.pdu import Address -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication -from bacpypes.object import get_object_class, get_datatype - -from bacpypes.apdu import ReadPropertyRequest, Error, AbortPDU, ReadPropertyACK +from bacpypes.apdu import ReadPropertyRequest, ReadPropertyACK from bacpypes.primitivedata import Unsigned from bacpypes.constructeddata import Array +from bacpypes.app import BIPSimpleApplication +from bacpypes.object import get_object_class, get_datatype +from bacpypes.service.device import LocalDeviceObject + # some debugging _debug = 0 _log = ModuleLogger(globals()) # globals this_application = None -this_console = None - -# -# ReadPropertyApplication -# - -@bacpypes_debugging -class ReadPropertyApplication(BIPSimpleApplication): - - def __init__(self, *args): - if _debug: ReadPropertyApplication._debug("__init__ %r", args) - BIPSimpleApplication.__init__(self, *args) - - # keep track of requests to line up responses - self._request = None - - def request(self, apdu): - if _debug: ReadPropertyApplication._debug("request %r", apdu) - - # save a copy of the request - self._request = apdu - - # forward it along - BIPSimpleApplication.request(self, apdu) - - def confirmation(self, apdu): - if _debug: ReadPropertyApplication._debug("confirmation %r", apdu) - - if isinstance(apdu, Error): - sys.stdout.write("error: %s\n" % (apdu.errorCode,)) - sys.stdout.flush() - - elif isinstance(apdu, AbortPDU): - apdu.debug_contents() - - elif (isinstance(self._request, ReadPropertyRequest)) and (isinstance(apdu, ReadPropertyACK)): - # find the datatype - datatype = get_datatype(apdu.objectIdentifier[0], apdu.propertyIdentifier) - if _debug: ReadPropertyApplication._debug(" - datatype: %r", datatype) - if not datatype: - raise TypeError("unknown datatype") - - # special case for array parts, others are managed by cast_out - if issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): - if apdu.propertyArrayIndex == 0: - value = apdu.propertyValue.cast_out(Unsigned) - else: - value = apdu.propertyValue.cast_out(datatype.subtype) - else: - value = apdu.propertyValue.cast_out(datatype) - if _debug: ReadPropertyApplication._debug(" - value: %r", value) - - sys.stdout.write(str(value) + '\n') - if hasattr(value, 'debug_contents'): - value.debug_contents(file=sys.stdout) - sys.stdout.flush() # @@ -123,8 +69,53 @@ def do_read(self, args): request.propertyArrayIndex = int(args[4]) if _debug: ReadPropertyConsoleCmd._debug(" - request: %r", request) + # make an IOCB + iocb = IOCB(request) + if _debug: ReadPropertyConsoleCmd._debug(" - iocb: %r", iocb) + # give it to the application - this_application.request(request) + this_application.request_io(iocb) + + # wait for it to complete + iocb.wait() + + # do something for error/reject/abort + if iocb.ioError: + sys.stdout.write(str(iocb.ioError) + '\n') + + # do something for success + elif iocb.ioResponse: + apdu = iocb.ioResponse + + # should be an ack + if not isinstance(apdu, ReadPropertyACK): + if _debug: ReadPropertyConsoleCmd._debug(" - not an ack") + return + + # find the datatype + datatype = get_datatype(apdu.objectIdentifier[0], apdu.propertyIdentifier) + if _debug: ReadPropertyConsoleCmd._debug(" - datatype: %r", datatype) + if not datatype: + raise TypeError("unknown datatype") + + # special case for array parts, others are managed by cast_out + if issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): + if apdu.propertyArrayIndex == 0: + value = apdu.propertyValue.cast_out(Unsigned) + else: + value = apdu.propertyValue.cast_out(datatype.subtype) + else: + value = apdu.propertyValue.cast_out(datatype) + if _debug: ReadPropertyConsoleCmd._debug(" - value: %r", value) + + sys.stdout.write(str(value) + '\n') + if hasattr(value, 'debug_contents'): + value.debug_contents(file=sys.stdout) + sys.stdout.flush() + + # do something with nothing? + else: + if _debug: ReadPropertyConsoleCmd._debug(" - ioError or ioResponse expected") except Exception as error: ReadPropertyConsoleCmd._exception("exception: %r", error) @@ -169,7 +160,7 @@ def main(): ) # make a simple application - this_application = ReadPropertyApplication(this_device, args.ini.address) + this_application = BIPSimpleApplication(this_device, args.ini.address) # get the services supported services_supported = this_application.get_services_supported() @@ -180,6 +171,7 @@ def main(): # make a console this_console = ReadPropertyConsoleCmd() + if _debug: _log.debug(" - this_console: %r", this_console) # enable sleeping will help with threads enable_sleeping() diff --git a/samples/ReadProperty25.py b/samples/ReadProperty25.py new file mode 100755 index 00000000..7924adff --- /dev/null +++ b/samples/ReadProperty25.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python + +""" +This application presents a 'console' prompt to the user asking for read commands +which create ReadPropertyRequest PDUs, then lines up the coorresponding ReadPropertyACK +and prints the value. +""" + +import sys + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser +from bacpypes.consolecmd import ConsoleCmd + +from bacpypes.core import run, enable_sleeping +from bacpypes.iocb import IOCB + +from bacpypes.pdu import Address +from bacpypes.apdu import ReadPropertyRequest, ReadPropertyACK +from bacpypes.primitivedata import Unsigned +from bacpypes.constructeddata import Array + +from bacpypes.app import BIPSimpleApplication +from bacpypes.object import get_object_class, get_datatype +from bacpypes.service.device import LocalDeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +this_application = None + + +# +# ReadPropertyConsoleCmd +# + +class ReadPropertyConsoleCmd(ConsoleCmd): + + def do_read(self, args): + """read [ ]""" + args = args.split() + if _debug: ReadPropertyConsoleCmd._debug("do_read %r", args) + + try: + addr, obj_type, obj_inst, prop_id = args[:4] + + if obj_type.isdigit(): + obj_type = int(obj_type) + elif not get_object_class(obj_type): + raise ValueError("unknown object type") + + obj_inst = int(obj_inst) + + datatype = get_datatype(obj_type, prop_id) + if not datatype: + raise ValueError("invalid property for object type") + + # build a request + request = ReadPropertyRequest( + objectIdentifier=(obj_type, obj_inst), + propertyIdentifier=prop_id, + ) + request.pduDestination = Address(addr) + + if len(args) == 5: + request.propertyArrayIndex = int(args[4]) + if _debug: ReadPropertyConsoleCmd._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) + if _debug: ReadPropertyConsoleCmd._debug(" - iocb: %r", iocb) + + # give it to the application + this_application.request_io(iocb) + + # wait for it to complete + iocb.wait() + + # do something for success + if iocb.ioResponse: + apdu = iocb.ioResponse + + # should be an ack + if not isinstance(apdu, ReadPropertyACK): + if _debug: ReadPropertyConsoleCmd._debug(" - not an ack") + return + + # find the datatype + datatype = get_datatype(apdu.objectIdentifier[0], apdu.propertyIdentifier) + if _debug: ReadPropertyConsoleCmd._debug(" - datatype: %r", datatype) + if not datatype: + raise TypeError("unknown datatype") + + # special case for array parts, others are managed by cast_out + if issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): + if apdu.propertyArrayIndex == 0: + value = apdu.propertyValue.cast_out(Unsigned) + else: + value = apdu.propertyValue.cast_out(datatype.subtype) + else: + value = apdu.propertyValue.cast_out(datatype) + if _debug: ReadPropertyConsoleCmd._debug(" - value: %r", value) + + sys.stdout.write(str(value) + '\n') + if hasattr(value, 'debug_contents'): + value.debug_contents(file=sys.stdout) + sys.stdout.flush() + + # do something for error/reject/abort + if iocb.ioError: + sys.stdout.write(str(iocb.ioError) + '\n') + + except Exception, error: + ReadPropertyConsoleCmd._exception("exception: %r", error) + + def do_rtn(self, args): + """rtn ... """ + args = args.split() + if _debug: ReadPropertyConsoleCmd._debug("do_rtn %r", args) + + # safe to assume only one adapter + adapter = this_application.nsap.adapters[0] + if _debug: ReadPropertyConsoleCmd._debug(" - adapter: %r", adapter) + + # provide the address and a list of network numbers + router_address = Address(args[0]) + network_list = [int(arg) for arg in args[1:]] + + # pass along to the service access point + this_application.nsap.add_router_references(adapter, router_address, network_list) + +bacpypes_debugging(ReadPropertyConsoleCmd) + + +# +# __main__ +# + +def main(): + global this_application + + # check the version + if (sys.version_info[:2] != (2, 5)): + sys.stderr.write("Python 2.5 only\n") + sys.exit(1) + + # parse the command line arguments + args = ConfigArgumentParser(description=__doc__).parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # 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), + ) + + # make a simple application + this_application = BIPSimpleApplication(this_device, args.ini.address) + + # 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 + + # make a console + this_console = ReadPropertyConsoleCmd() + if _debug: _log.debug(" - this_console: %r", this_console) + + # enable sleeping will help with threads + enable_sleeping() + + _log.debug("running") + + run() + + _log.debug("fini") + +if __name__ == "__main__": + main() diff --git a/samples/ReadPropertyAny.py b/samples/ReadPropertyAny.py index 3e62fd82..44cac18b 100755 --- a/samples/ReadPropertyAny.py +++ b/samples/ReadPropertyAny.py @@ -2,8 +2,9 @@ """ This application presents a 'console' prompt to the user asking for read commands -which create ReadPropertyRequest PDUs, then lines up the coorresponding ReadPropertyACK -and prints the value. +which create ReadPropertyRequest PDUs, waits for the response, then decodes the +value if it is application encoded. This is useful for reading the values +of propietary properties when the datatype isn't known. """ import sys @@ -13,14 +14,18 @@ from bacpypes.consolecmd import ConsoleCmd from bacpypes.core import run, enable_sleeping +from bacpypes.iocb import IOCB from bacpypes.pdu import Address -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication -from bacpypes.object import get_object_class +from bacpypes.object import get_datatype, get_object_class from bacpypes.apdu import ReadPropertyRequest, Error, AbortPDU, ReadPropertyACK from bacpypes.primitivedata import Tag +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject + + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -28,64 +33,6 @@ # globals this_application = None -# -# ReadPropertyAnyApplication -# - -@bacpypes_debugging -class ReadPropertyAnyApplication(BIPSimpleApplication): - - def __init__(self, *args): - if _debug: ReadPropertyAnyApplication._debug("__init__ %r", args) - BIPSimpleApplication.__init__(self, *args) - - # keep track of requests to line up responses - self._request = None - - def request(self, apdu): - if _debug: ReadPropertyAnyApplication._debug("request %r", apdu) - - # save a copy of the request - self._request = apdu - - # forward it along - BIPSimpleApplication.request(self, apdu) - - def confirmation(self, apdu): - if _debug: ReadPropertyAnyApplication._debug("confirmation %r", apdu) - - if isinstance(apdu, Error): - sys.stdout.write("error: %s\n" % (apdu.errorCode,)) - sys.stdout.flush() - - elif isinstance(apdu, AbortPDU): - apdu.debug_contents() - - elif (isinstance(self._request, ReadPropertyRequest)) and (isinstance(apdu, ReadPropertyACK)): - # peek at the value tag - value_tag = apdu.propertyValue.tagList.Peek() - if _debug: ReadPropertyAnyApplication._debug(" - value_tag: %r", value_tag) - - # make sure that it is application tagged - if value_tag.tagClass != Tag.applicationTagClass: - sys.stdout.write("value is not application encoded\n") - - else: - # find the datatype - datatype = Tag._app_tag_class[value_tag.tagNumber] - if _debug: ReadPropertyAnyApplication._debug(" - datatype: %r", datatype) - if not datatype: - raise TypeError("unknown datatype") - - # cast out the value - value = apdu.propertyValue.cast_out(datatype) - if _debug: ReadPropertyAnyApplication._debug(" - value: %r", value) - - sys.stdout.write(str(value) + '\n') - - sys.stdout.flush() - - # # ReadPropertyAnyConsoleCmd # @@ -108,9 +55,6 @@ def do_read(self, args): obj_inst = int(obj_inst) - if prop_id.isdigit(): - prop_id = int(prop_id) - # build a request request = ReadPropertyRequest( objectIdentifier=(obj_type, obj_inst), @@ -122,13 +66,50 @@ def do_read(self, args): request.propertyArrayIndex = int(args[4]) if _debug: ReadPropertyAnyConsoleCmd._debug(" - request: %r", request) + # make an IOCB + iocb = IOCB(request) + if _debug: ReadPropertyAnyConsoleCmd._debug(" - iocb: %r", iocb) + # give it to the application - this_application.request(request) + this_application.request_io(iocb) + + # wait for it to complete + iocb.wait() + + # do something for success + if iocb.ioResponse: + apdu = iocb.ioResponse + + # peek at the value tag + value_tag = apdu.propertyValue.tagList.Peek() + if _debug: ReadPropertyAnyConsoleCmd._debug(" - value_tag: %r", value_tag) + + # make sure that it is application tagged + if value_tag.tagClass != Tag.applicationTagClass: + sys.stdout.write("value is not application encoded\n") + + else: + # find the datatype + datatype = Tag._app_tag_class[value_tag.tagNumber] + if _debug: ReadPropertyAnyConsoleCmd._debug(" - datatype: %r", datatype) + if not datatype: + raise TypeError("unknown datatype") + + # cast out the value + value = apdu.propertyValue.cast_out(datatype) + if _debug: ReadPropertyAnyConsoleCmd._debug(" - value: %r", value) + + sys.stdout.write("%s (%s)\n" % (value, datatype)) + + sys.stdout.flush() + + # do something for error/reject/abort + if iocb.ioError: + sys.stdout.write(str(iocb.ioError) + '\n') except Exception as error: ReadPropertyAnyConsoleCmd._exception("exception: %r", error) - # # __main__ # @@ -152,7 +133,7 @@ def main(): ) # make a simple application - this_application = ReadPropertyAnyApplication(this_device, args.ini.address) + this_application = BIPSimpleApplication(this_device, args.ini.address) if _debug: _log.debug(" - this_application: %r", this_application) # get the services supported diff --git a/samples/ReadPropertyMultiple.py b/samples/ReadPropertyMultiple.py index 01514475..f355f8d8 100755 --- a/samples/ReadPropertyMultiple.py +++ b/samples/ReadPropertyMultiple.py @@ -13,106 +13,26 @@ from bacpypes.consolecmd import ConsoleCmd from bacpypes.core import run, enable_sleeping +from bacpypes.iocb import IOCB from bacpypes.pdu import Address -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication from bacpypes.object import get_object_class, get_datatype -from bacpypes.apdu import ReadPropertyMultipleRequest, PropertyReference, ReadAccessSpecification, Error, AbortPDU, ReadPropertyMultipleACK +from bacpypes.apdu import ReadPropertyMultipleRequest, PropertyReference, \ + ReadAccessSpecification, ReadPropertyMultipleACK from bacpypes.primitivedata import Unsigned from bacpypes.constructeddata import Array from bacpypes.basetypes import PropertyIdentifier +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject + # some debugging _debug = 0 _log = ModuleLogger(globals()) # globals -this_device = None this_application = None -this_console = None - -# -# ReadPropertyMultipleApplication -# - -@bacpypes_debugging -class ReadPropertyMultipleApplication(BIPSimpleApplication): - - def __init__(self, *args): - if _debug: ReadPropertyMultipleApplication._debug("__init__ %r", args) - BIPSimpleApplication.__init__(self, *args) - - # keep track of requests to line up responses - self._request = None - - def request(self, apdu): - if _debug: ReadPropertyMultipleApplication._debug("request %r", apdu) - - # save a copy of the request - self._request = apdu - - # forward it along - BIPSimpleApplication.request(self, apdu) - - def confirmation(self, apdu): - if _debug: ReadPropertyMultipleApplication._debug("confirmation %r", apdu) - - if isinstance(apdu, Error): - sys.stdout.write("error: %s\n" % (apdu.errorCode,)) - sys.stdout.flush() - - elif isinstance(apdu, AbortPDU): - apdu.debug_contents() - - elif (isinstance(self._request, ReadPropertyMultipleRequest)) and (isinstance(apdu, ReadPropertyMultipleACK)): - # loop through the results - for result in apdu.listOfReadAccessResults: - # here is the object identifier - objectIdentifier = result.objectIdentifier - if _debug: ReadPropertyMultipleApplication._debug(" - objectIdentifier: %r", objectIdentifier) - - # now come the property values per object - for element in result.listOfResults: - # get the property and array index - propertyIdentifier = element.propertyIdentifier - if _debug: ReadPropertyMultipleApplication._debug(" - propertyIdentifier: %r", propertyIdentifier) - propertyArrayIndex = element.propertyArrayIndex - if _debug: ReadPropertyMultipleApplication._debug(" - propertyArrayIndex: %r", propertyArrayIndex) - - # here is the read result - readResult = element.readResult - - sys.stdout.write(propertyIdentifier) - if propertyArrayIndex is not None: - sys.stdout.write("[" + str(propertyArrayIndex) + "]") - - # check for an error - if readResult.propertyAccessError is not None: - sys.stdout.write(" ! " + str(readResult.propertyAccessError) + '\n') - - else: - # here is the value - propertyValue = readResult.propertyValue - - # find the datatype - datatype = get_datatype(objectIdentifier[0], propertyIdentifier) - if _debug: ReadPropertyMultipleApplication._debug(" - datatype: %r", datatype) - if not datatype: - raise TypeError("unknown datatype") - - # special case for array parts, others are managed by cast_out - if issubclass(datatype, Array) and (propertyArrayIndex is not None): - if propertyArrayIndex == 0: - value = propertyValue.cast_out(Unsigned) - else: - value = propertyValue.cast_out(datatype.subtype) - else: - value = propertyValue.cast_out(datatype) - if _debug: ReadPropertyMultipleApplication._debug(" - value: %r", value) - - sys.stdout.write(" = " + str(value) + '\n') - sys.stdout.flush() # # ReadPropertyMultipleConsoleCmd @@ -140,7 +60,7 @@ def do_read(self, args): obj_type = int(obj_type) elif not get_object_class(obj_type): raise ValueError("unknown object type") - + obj_inst = int(args[i]) i += 1 @@ -195,8 +115,76 @@ def do_read(self, args): request.pduDestination = Address(addr) if _debug: ReadPropertyMultipleConsoleCmd._debug(" - request: %r", request) + # make an IOCB + iocb = IOCB(request) + if _debug: ReadPropertyMultipleConsoleCmd._debug(" - iocb: %r", iocb) + # give it to the application - this_application.request(request) + this_application.request_io(iocb) + + # wait for it to complete + iocb.wait() + + # do something for success + if iocb.ioResponse: + apdu = iocb.ioResponse + + # should be an ack + if not isinstance(apdu, ReadPropertyMultipleACK): + if _debug: ReadPropertyMultipleConsoleCmd._debug(" - not an ack") + return + + # loop through the results + for result in apdu.listOfReadAccessResults: + # here is the object identifier + objectIdentifier = result.objectIdentifier + if _debug: ReadPropertyMultipleConsoleCmd._debug(" - objectIdentifier: %r", objectIdentifier) + + # now come the property values per object + for element in result.listOfResults: + # get the property and array index + propertyIdentifier = element.propertyIdentifier + if _debug: ReadPropertyMultipleConsoleCmd._debug(" - propertyIdentifier: %r", propertyIdentifier) + propertyArrayIndex = element.propertyArrayIndex + if _debug: ReadPropertyMultipleConsoleCmd._debug(" - propertyArrayIndex: %r", propertyArrayIndex) + + # here is the read result + readResult = element.readResult + + sys.stdout.write(propertyIdentifier) + if propertyArrayIndex is not None: + sys.stdout.write("[" + str(propertyArrayIndex) + "]") + + # check for an error + if readResult.propertyAccessError is not None: + sys.stdout.write(" ! " + str(readResult.propertyAccessError) + '\n') + + else: + # here is the value + propertyValue = readResult.propertyValue + + # find the datatype + datatype = get_datatype(objectIdentifier[0], propertyIdentifier) + if _debug: ReadPropertyMultipleConsoleCmd._debug(" - datatype: %r", datatype) + if not datatype: + raise TypeError("unknown datatype") + + # special case for array parts, others are managed by cast_out + if issubclass(datatype, Array) and (propertyArrayIndex is not None): + if propertyArrayIndex == 0: + value = propertyValue.cast_out(Unsigned) + else: + value = propertyValue.cast_out(datatype.subtype) + else: + value = propertyValue.cast_out(datatype) + if _debug: ReadPropertyMultipleConsoleCmd._debug(" - value: %r", value) + + sys.stdout.write(" = " + str(value) + '\n') + sys.stdout.flush() + + # do something for error/reject/abort + if iocb.ioError: + sys.stdout.write(str(iocb.ioError) + '\n') except Exception as error: ReadPropertyMultipleConsoleCmd._exception("exception: %r", error) @@ -224,7 +212,7 @@ def main(): ) # make a simple application - this_application = ReadPropertyMultipleApplication(this_device, args.ini.address) + this_application = BIPSimpleApplication(this_device, args.ini.address) # get the services supported services_supported = this_application.get_services_supported() diff --git a/samples/ReadPropertyMultiple25.py b/samples/ReadPropertyMultiple25.py new file mode 100755 index 00000000..cd53c5db --- /dev/null +++ b/samples/ReadPropertyMultiple25.py @@ -0,0 +1,245 @@ +#!/usr/bin/env python + +""" +This application presents a 'console' prompt to the user asking for read commands +which create ReadPropertyRequest PDUs, then lines up the coorresponding ReadPropertyACK +and prints the value. +""" + +import sys + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser +from bacpypes.consolecmd import ConsoleCmd + +from bacpypes.core import run, enable_sleeping +from bacpypes.iocb import IOCB + +from bacpypes.pdu import Address +from bacpypes.object import get_object_class, get_datatype + +from bacpypes.apdu import ReadPropertyMultipleRequest, PropertyReference, \ + ReadAccessSpecification, ReadPropertyMultipleACK +from bacpypes.primitivedata import Unsigned +from bacpypes.constructeddata import Array +from bacpypes.basetypes import PropertyIdentifier + +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +this_application = None + +# +# ReadPropertyMultipleConsoleCmd +# + +class ReadPropertyMultipleConsoleCmd(ConsoleCmd): + + def do_read(self, args): + """read ( ( [ ] )... )...""" + args = args.split() + if _debug: ReadPropertyMultipleConsoleCmd._debug("do_read %r", args) + + try: + i = 0 + addr = args[i] + i += 1 + + read_access_spec_list = [] + while i < len(args): + obj_type = args[i] + i += 1 + + if obj_type.isdigit(): + obj_type = int(obj_type) + elif not get_object_class(obj_type): + raise ValueError("unknown object type") + + obj_inst = int(args[i]) + i += 1 + + prop_reference_list = [] + while i < len(args): + prop_id = args[i] + if prop_id not in PropertyIdentifier.enumerations: + break + + i += 1 + if prop_id in ('all', 'required', 'optional'): + pass + else: + datatype = get_datatype(obj_type, prop_id) + if not datatype: + raise ValueError("invalid property for object type") + + # build a property reference + prop_reference = PropertyReference( + propertyIdentifier=prop_id, + ) + + # check for an array index + if (i < len(args)) and args[i].isdigit(): + prop_reference.propertyArrayIndex = int(args[i]) + i += 1 + + # add it to the list + prop_reference_list.append(prop_reference) + + # check for at least one property + if not prop_reference_list: + raise ValueError("provide at least one property") + + # build a read access specification + read_access_spec = ReadAccessSpecification( + objectIdentifier=(obj_type, obj_inst), + listOfPropertyReferences=prop_reference_list, + ) + + # add it to the list + read_access_spec_list.append(read_access_spec) + + # check for at least one + if not read_access_spec_list: + raise RuntimeError("at least one read access specification required") + + # build the request + request = ReadPropertyMultipleRequest( + listOfReadAccessSpecs=read_access_spec_list, + ) + request.pduDestination = Address(addr) + if _debug: ReadPropertyMultipleConsoleCmd._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) + if _debug: ReadPropertyMultipleConsoleCmd._debug(" - iocb: %r", iocb) + + # give it to the application + this_application.request_io(iocb) + + # wait for it to complete + iocb.wait() + + # do something for success + if iocb.ioResponse: + apdu = iocb.ioResponse + + # should be an ack + if not isinstance(apdu, ReadPropertyMultipleACK): + if _debug: ReadPropertyMultipleConsoleCmd._debug(" - not an ack") + return + + # loop through the results + for result in apdu.listOfReadAccessResults: + # here is the object identifier + objectIdentifier = result.objectIdentifier + if _debug: ReadPropertyMultipleConsoleCmd._debug(" - objectIdentifier: %r", objectIdentifier) + + # now come the property values per object + for element in result.listOfResults: + # get the property and array index + propertyIdentifier = element.propertyIdentifier + if _debug: ReadPropertyMultipleConsoleCmd._debug(" - propertyIdentifier: %r", propertyIdentifier) + propertyArrayIndex = element.propertyArrayIndex + if _debug: ReadPropertyMultipleConsoleCmd._debug(" - propertyArrayIndex: %r", propertyArrayIndex) + + # here is the read result + readResult = element.readResult + + sys.stdout.write(propertyIdentifier) + if propertyArrayIndex is not None: + sys.stdout.write("[" + str(propertyArrayIndex) + "]") + + # check for an error + if readResult.propertyAccessError is not None: + sys.stdout.write(" ! " + str(readResult.propertyAccessError) + '\n') + + else: + # here is the value + propertyValue = readResult.propertyValue + + # find the datatype + datatype = get_datatype(objectIdentifier[0], propertyIdentifier) + if _debug: ReadPropertyMultipleConsoleCmd._debug(" - datatype: %r", datatype) + if not datatype: + raise TypeError("unknown datatype") + + # special case for array parts, others are managed by cast_out + if issubclass(datatype, Array) and (propertyArrayIndex is not None): + if propertyArrayIndex == 0: + value = propertyValue.cast_out(Unsigned) + else: + value = propertyValue.cast_out(datatype.subtype) + else: + value = propertyValue.cast_out(datatype) + if _debug: ReadPropertyMultipleConsoleCmd._debug(" - value: %r", value) + + sys.stdout.write(" = " + str(value) + '\n') + sys.stdout.flush() + + # do something for error/reject/abort + if iocb.ioError: + sys.stdout.write(str(iocb.ioError) + '\n') + + except Exception, error: + ReadPropertyMultipleConsoleCmd._exception("exception: %r", error) + +bacpypes_debugging(ReadPropertyMultipleConsoleCmd) + +# +# __main__ +# + +def main(): + global this_application + + # check the version + if (sys.version_info[:2] != (2, 5)): + sys.stderr.write("Python 2.5 only\n") + sys.exit(1) + + # parse the command line arguments + args = ConfigArgumentParser(description=__doc__).parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # 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), + ) + + # make a simple application + this_application = BIPSimpleApplication(this_device, args.ini.address) + + # 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 + + # make a console + this_console = ReadPropertyMultipleConsoleCmd() + if _debug: _log.debug(" - this_console: %r", this_console) + + # enable sleeping will help with threads + enable_sleeping() + + _log.debug("running") + + run() + + _log.debug("fini") + + +if __name__ == "__main__": + main() diff --git a/samples/ReadPropertyMultipleServer.py b/samples/ReadPropertyMultipleServer.py index 9ee8f212..2af5c3d2 100755 --- a/samples/ReadPropertyMultipleServer.py +++ b/samples/ReadPropertyMultipleServer.py @@ -1,7 +1,7 @@ #!/usr/bin/env python """ -This sample application shows how to extend the basic functionality of a device +This sample application shows how to extend the basic functionality of a device to support the ReadPropertyMultiple service. """ @@ -12,19 +12,26 @@ from bacpypes.core import run -from bacpypes.primitivedata import Atomic, Real, Unsigned -from bacpypes.constructeddata import Array, Any -from bacpypes.basetypes import ErrorType -from bacpypes.apdu import ReadPropertyMultipleACK, ReadAccessResult, ReadAccessResultElement, ReadAccessResultElementChoice -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication -from bacpypes.object import AnalogValueObject, Property, PropertyError, register_object_type -from bacpypes.apdu import Error +from bacpypes.primitivedata import Real +from bacpypes.object import AnalogValueObject, Property, register_object_type from bacpypes.errors import ExecutionError +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import ReadWritePropertyMultipleServices, \ + LocalDeviceObject + # some debugging _debug = 0 _log = ModuleLogger(globals()) +# +# ReadPropertyMultipleApplication +# + +@bacpypes_debugging +class ReadPropertyMultipleApplication(BIPSimpleApplication, ReadWritePropertyMultipleServices): + pass + # # RandomValueProperty # @@ -70,190 +77,6 @@ def __init__(self, **kwargs): register_object_type(RandomAnalogValueObject) -# -# ReadPropertyToAny -# - -@bacpypes_debugging -def ReadPropertyToAny(obj, propertyIdentifier, propertyArrayIndex=None): - """Read the specified property of the object, with the optional array index, - and cast the result into an Any object.""" - if _debug: ReadPropertyToAny._debug("ReadPropertyToAny %s %r %r", obj, propertyIdentifier, propertyArrayIndex) - - # get the datatype - datatype = obj.get_datatype(propertyIdentifier) - if _debug: ReadPropertyToAny._debug(" - datatype: %r", datatype) - if datatype is None: - raise ExecutionError(errorClass='property', errorCode='datatypeNotSupported') - - # get the value - value = obj.ReadProperty(propertyIdentifier, propertyArrayIndex) - if _debug: ReadPropertyToAny._debug(" - value: %r", value) - if value is None: - raise ExecutionError(errorClass='property', errorCode='unknownProperty') - - # change atomic values into something encodeable - if issubclass(datatype, Atomic): - value = datatype(value) - elif issubclass(datatype, Array) and (propertyArrayIndex is not None): - if propertyArrayIndex == 0: - value = Unsigned(value) - elif issubclass(datatype.subtype, Atomic): - value = datatype.subtype(value) - elif not isinstance(value, datatype.subtype): - raise TypeError("invalid result datatype, expecting %s and got %s" \ - % (datatype.subtype.__name__, type(value).__name__)) - elif not isinstance(value, datatype): - raise TypeError("invalid result datatype, expecting %s and got %s" \ - % (datatype.__name__, type(value).__name__)) - if _debug: ReadPropertyToAny._debug(" - encodeable value: %r", value) - - # encode the value - result = Any() - result.cast_in(value) - if _debug: ReadPropertyToAny._debug(" - result: %r", result) - - # return the object - return result - -# -# ReadPropertyToResultElement -# - -@bacpypes_debugging -def ReadPropertyToResultElement(obj, propertyIdentifier, propertyArrayIndex=None): - """Read the specified property of the object, with the optional array index, - and cast the result into an Any object.""" - if _debug: ReadPropertyToResultElement._debug("ReadPropertyToResultElement %s %r %r", obj, propertyIdentifier, propertyArrayIndex) - - # save the result in the property value - read_result = ReadAccessResultElementChoice() - - try: - read_result.propertyValue = ReadPropertyToAny(obj, propertyIdentifier, propertyArrayIndex) - if _debug: ReadPropertyToResultElement._debug(" - success") - except PropertyError as error: - if _debug: ReadPropertyToResultElement._debug(" - error: %r", error) - read_result.propertyAccessError = ErrorType(errorClass='property', errorCode='unknownProperty') - except ExecutionError as error: - if _debug: ReadPropertyToResultElement._debug(" - error: %r", error) - read_result.propertyAccessError = ErrorType(errorClass=error.errorClass, errorCode=error.errorCode) - - # make an element for this value - read_access_result_element = ReadAccessResultElement( - propertyIdentifier=propertyIdentifier, - propertyArrayIndex=propertyArrayIndex, - readResult=read_result, - ) - if _debug: ReadPropertyToResultElement._debug(" - read_access_result_element: %r", read_access_result_element) - - # fini - return read_access_result_element - -# -# ReadPropertyMultipleApplication -# - -@bacpypes_debugging -class ReadPropertyMultipleApplication(BIPSimpleApplication): - - def __init__(self, *args, **kwargs): - if _debug: ReadPropertyMultipleApplication._debug("__init__ %r %r", args, kwargs) - BIPSimpleApplication.__init__(self, *args, **kwargs) - - def do_ReadPropertyMultipleRequest(self, apdu): - """Respond to a ReadPropertyMultiple Request.""" - if _debug: ReadPropertyMultipleApplication._debug("do_ReadPropertyMultipleRequest %r", apdu) - - # response is a list of read access results (or an error) - resp = None - read_access_result_list = [] - - # loop through the request - for read_access_spec in apdu.listOfReadAccessSpecs: - # get the object identifier - objectIdentifier = read_access_spec.objectIdentifier - if _debug: ReadPropertyMultipleApplication._debug(" - objectIdentifier: %r", objectIdentifier) - - # check for wildcard - if (objectIdentifier == ('device', 4194303)): - if _debug: ReadPropertyMultipleApplication._debug(" - wildcard device identifier") - objectIdentifier = self.localDevice.objectIdentifier - - # get the object - obj = self.get_object_id(objectIdentifier) - if _debug: ReadPropertyMultipleApplication._debug(" - object: %r", obj) - - # make sure it exists - if not obj: - resp = Error(errorClass='object', errorCode='unknownObject', context=apdu) - if _debug: ReadPropertyMultipleApplication._debug(" - unknown object error: %r", resp) - break - - # build a list of result elements - read_access_result_element_list = [] - - # loop through the property references - for prop_reference in read_access_spec.listOfPropertyReferences: - # get the property identifier - propertyIdentifier = prop_reference.propertyIdentifier - if _debug: ReadPropertyMultipleApplication._debug(" - propertyIdentifier: %r", propertyIdentifier) - - # get the array index (optional) - propertyArrayIndex = prop_reference.propertyArrayIndex - if _debug: ReadPropertyMultipleApplication._debug(" - propertyArrayIndex: %r", propertyArrayIndex) - - # check for special property identifiers - if propertyIdentifier in ('all', 'required', 'optional'): - for propId, prop in obj._properties.items(): - if _debug: ReadPropertyMultipleApplication._debug(" - checking: %r %r", propId, prop.optional) - - if (propertyIdentifier == 'all'): - pass - elif (propertyIdentifier == 'required') and (prop.optional): - if _debug: ReadPropertyMultipleApplication._debug(" - not a required property") - continue - elif (propertyIdentifier == 'optional') and (not prop.optional): - if _debug: ReadPropertyMultipleApplication._debug(" - not an optional property") - continue - - # read the specific property - read_access_result_element = ReadPropertyToResultElement(obj, propId, propertyArrayIndex) - - # check for undefined property - if read_access_result_element.readResult.propertyAccessError \ - and read_access_result_element.readResult.propertyAccessError.errorCode == 'unknownProperty': - continue - - # add it to the list - read_access_result_element_list.append(read_access_result_element) - - else: - # read the specific property - read_access_result_element = ReadPropertyToResultElement(obj, propertyIdentifier, propertyArrayIndex) - - # add it to the list - read_access_result_element_list.append(read_access_result_element) - - # build a read access result - read_access_result = ReadAccessResult( - objectIdentifier=objectIdentifier, - listOfResults=read_access_result_element_list - ) - if _debug: ReadPropertyMultipleApplication._debug(" - read_access_result: %r", read_access_result) - - # add it to the list - read_access_result_list.append(read_access_result) - - # this is a ReadPropertyMultiple ack - if not resp: - resp = ReadPropertyMultipleACK(context=apdu) - resp.listOfReadAccessResults = read_access_result_list - if _debug: ReadPropertyMultipleApplication._debug(" - resp: %r", resp) - - # return the result - self.response(resp) - # # __main__ # diff --git a/samples/ReadPropertyMultipleServer25.py b/samples/ReadPropertyMultipleServer25.py new file mode 100755 index 00000000..4c97e13f --- /dev/null +++ b/samples/ReadPropertyMultipleServer25.py @@ -0,0 +1,140 @@ +#!/usr/bin/env python + +""" +This sample application shows how to extend the basic functionality of a device +to support the ReadPropertyMultiple service. +""" + +import sys +import random + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser + +from bacpypes.core import run + +from bacpypes.primitivedata import Real +from bacpypes.object import AnalogValueObject, Property, register_object_type +from bacpypes.errors import ExecutionError + +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import ReadWritePropertyMultipleServices, \ + LocalDeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# ReadPropertyMultipleApplication +# + +class ReadPropertyMultipleApplication(BIPSimpleApplication, ReadWritePropertyMultipleServices): + pass + +# +# RandomValueProperty +# + +class RandomValueProperty(Property): + + def __init__(self, identifier): + if _debug: RandomValueProperty._debug("__init__ %r", identifier) + Property.__init__(self, identifier, Real, default=None, optional=True, mutable=False) + + def ReadProperty(self, obj, arrayIndex=None): + if _debug: RandomValueProperty._debug("ReadProperty %r arrayIndex=%r", obj, arrayIndex) + + # access an array + if arrayIndex is not None: + raise ExecutionError(errorClass='property', errorCode='propertyIsNotAnArray') + + # return a random value + value = random.random() * 100.0 + if _debug: RandomValueProperty._debug(" - value: %r", value) + + return value + + def WriteProperty(self, obj, value, arrayIndex=None, priority=None, direct=False): + if _debug: RandomValueProperty._debug("WriteProperty %r %r arrayIndex=%r priority=%r direct=%r", obj, value, arrayIndex, priority, direct) + raise ExecutionError(errorClass='property', errorCode='writeAccessDenied') + +bacpypes_debugging(RandomValueProperty) + +# +# Random Value Object Type +# + +class RandomAnalogValueObject(AnalogValueObject): + + properties = [ + RandomValueProperty('presentValue'), + ] + + def __init__(self, **kwargs): + if _debug: RandomAnalogValueObject._debug("__init__ %r", kwargs) + AnalogValueObject.__init__(self, **kwargs) + +bacpypes_debugging(RandomAnalogValueObject) +register_object_type(RandomAnalogValueObject) + +# +# __main__ +# + +def main(): + # check the version + if (sys.version_info[:2] != (2, 5)): + sys.stderr.write("Python 2.5 only\n") + sys.exit(1) + + # parse the command line arguments + args = ConfigArgumentParser(description=__doc__).parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # 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), + ) + + # make a sample application + this_application = ReadPropertyMultipleApplication(this_device, args.ini.address) + + # make a random input object + ravo1 = RandomAnalogValueObject( + objectIdentifier=('analogValue', 1), objectName='Random1' + ) + _log.debug(" - ravo1: %r", ravo1) + + ravo2 = RandomAnalogValueObject( + objectIdentifier=('analogValue', 2), objectName='Random2' + ) + _log.debug(" - ravo2: %r", ravo2) + + # add it to the device + this_application.add_object(ravo1) + this_application.add_object(ravo2) + _log.debug(" - object list: %r", this_device.objectList) + + # 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 + + _log.debug("running") + + run() + + _log.debug("fini") + + +if __name__ == "__main__": + main() diff --git a/samples/ReadRange.py b/samples/ReadRange.py index fe4fb27f..b75e43a8 100755 --- a/samples/ReadRange.py +++ b/samples/ReadRange.py @@ -13,11 +13,14 @@ from bacpypes.consolecmd import ConsoleCmd from bacpypes.core import run, enable_sleeping +from bacpypes.iocb import IOCB from bacpypes.pdu import Address -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication from bacpypes.object import get_object_class, get_datatype -from bacpypes.apdu import Error, AbortPDU, ReadRangeRequest, ReadRangeACK +from bacpypes.apdu import ReadRangeRequest, ReadRangeACK + +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject # some debugging _debug = 0 @@ -26,55 +29,6 @@ # globals this_application = None -# -# ReadRangeApplication -# - -@bacpypes_debugging -class ReadRangeApplication(BIPSimpleApplication): - - def __init__(self, *args): - if _debug: ReadRangeApplication._debug("__init__ %r", args) - BIPSimpleApplication.__init__(self, *args) - - # keep track of requests to line up responses - self._request = None - - def request(self, apdu): - if _debug: ReadRangeApplication._debug("request %r", apdu) - - # save a copy of the request - self._request = apdu - - # forward it along - BIPSimpleApplication.request(self, apdu) - - def confirmation(self, apdu): - if _debug: ReadRangeApplication._debug("confirmation %r", apdu) - - if isinstance(apdu, Error): - sys.stdout.write("error: %s\n" % (apdu.errorCode,)) - sys.stdout.flush() - - elif isinstance(apdu, AbortPDU): - apdu.debug_contents() - - elif (isinstance(self._request, ReadRangeRequest)) and (isinstance(apdu, ReadRangeACK)): - # find the datatype - datatype = get_datatype(apdu.objectIdentifier[0], apdu.propertyIdentifier) - if _debug: ReadRangeApplication._debug(" - datatype: %r", datatype) - if not datatype: - raise TypeError("unknown datatype") - - # cast out of the single Any element into the datatype - value = apdu.itemData[0].cast_out(datatype) - - # dump it out - for i, item in enumerate(value): - sys.stdout.write("[%d]\n" % (i,)) - item.debug_contents(file=sys.stdout, indent=2) - sys.stdout.flush() - # # ReadRangeConsoleCmd # @@ -112,8 +66,43 @@ def do_readrange(self, args): request.propertyArrayIndex = int(args[4]) if _debug: ReadRangeConsoleCmd._debug(" - request: %r", request) + # make an IOCB + iocb = IOCB(request) + if _debug: ReadRangeConsoleCmd._debug(" - iocb: %r", iocb) + # give it to the application - this_application.request(request) + this_application.request_io(iocb) + + # wait for it to complete + iocb.wait() + + # do something for success + if iocb.ioResponse: + apdu = iocb.ioResponse + + # should be an ack + if not isinstance(apdu, ReadRangeACK): + if _debug: ReadRangeConsoleCmd._debug(" - not an ack") + return + + # find the datatype + datatype = get_datatype(apdu.objectIdentifier[0], apdu.propertyIdentifier) + if _debug: ReadRangeConsoleCmd._debug(" - datatype: %r", datatype) + if not datatype: + raise TypeError("unknown datatype") + + # cast out the data into a list + value = apdu.itemData[0].cast_out(datatype) + + # dump it out + for i, item in enumerate(value): + sys.stdout.write("[%d]\n" % (i,)) + item.debug_contents(file=sys.stdout, indent=2) + sys.stdout.flush() + + # do something for error/reject/abort + if iocb.ioError: + sys.stdout.write(str(iocb.ioError) + '\n') except Exception as error: ReadRangeConsoleCmd._exception("exception: %r", error) @@ -141,7 +130,7 @@ def main(): ) # make a simple application - this_application = ReadRangeApplication(this_device, args.ini.address) + this_application = BIPSimpleApplication(this_device, args.ini.address) # get the services supported services_supported = this_application.get_services_supported() @@ -152,6 +141,7 @@ def main(): # make a console this_console = ReadRangeConsoleCmd() + if _debug: _log.debug(" - this_console: %r", this_console) # enable sleeping will help with threads enable_sleeping() diff --git a/samples/ReadWriteFile.py b/samples/ReadWriteFile.py index 93205349..7cc3a19a 100755 --- a/samples/ReadWriteFile.py +++ b/samples/ReadWriteFile.py @@ -4,7 +4,7 @@ This application presents a 'console' prompt to the user asking for commands. The 'readrecord' and 'writerecord' commands are used with record oriented files, -and the 'readstream' and 'writestream' commands are used with stream oriented +and the 'readstream' and 'writestream' commands are used with stream oriented files. """ @@ -15,10 +15,9 @@ from bacpypes.consolecmd import ConsoleCmd from bacpypes.core import run, enable_sleeping +from bacpypes.iocb import IOCB from bacpypes.pdu import Address -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication - from bacpypes.apdu import Error, AbortPDU, \ AtomicReadFileRequest, \ AtomicReadFileRequestAccessMethodChoice, \ @@ -31,6 +30,9 @@ AtomicWriteFileRequestAccessMethodChoiceStreamAccess, \ AtomicWriteFileACK +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -38,54 +40,6 @@ # reference a simple application this_application = None -# -# TestApplication -# - -@bacpypes_debugging -class TestApplication(BIPSimpleApplication): - - def request(self, apdu): - if _debug: TestApplication._debug("request %r", apdu) - - # save a copy of the request - self._request = apdu - - # forward it along - BIPSimpleApplication.request(self, apdu) - - def confirmation(self, apdu): - if _debug: TestApplication._debug("confirmation %r", apdu) - - if isinstance(apdu, Error): - sys.stdout.write("error: %s\n" % (apdu.errorCode,)) - sys.stdout.flush() - - elif isinstance(apdu, AbortPDU): - apdu.debug_contents() - - elif (isinstance(self._request, AtomicReadFileRequest)) and (isinstance(apdu, AtomicReadFileACK)): - # suck out the record data - if apdu.accessMethod.recordAccess: - value = apdu.accessMethod.recordAccess.fileRecordData - elif apdu.accessMethod.streamAccess: - value = apdu.accessMethod.streamAccess.fileData - TestApplication._debug(" - value: %r", value) - - sys.stdout.write(repr(value) + '\n') - sys.stdout.flush() - - elif (isinstance(self._request, AtomicWriteFileRequest)) and (isinstance(apdu, AtomicWriteFileACK)): - # suck out the record data - if apdu.fileStartPosition is not None: - value = apdu.fileStartPosition - elif apdu.fileStartRecord is not None: - value = apdu.fileStartRecord - TestApplication._debug(" - value: %r", value) - - sys.stdout.write(repr(value) + '\n') - sys.stdout.flush() - # # TestConsoleCmd # @@ -119,8 +73,38 @@ def do_readrecord(self, args): request.pduDestination = Address(addr) if _debug: TestConsoleCmd._debug(" - request: %r", request) + # make an IOCB + iocb = IOCB(request) + if _debug: TestConsoleCmd._debug(" - iocb: %r", iocb) + # give it to the application - this_application.request(request) + this_application.request_io(iocb) + + # wait for it to complete + iocb.wait() + + # do something for success + if iocb.ioResponse: + apdu = iocb.ioResponse + + # should be an ack + if not isinstance(apdu, AtomicReadFileACK): + if _debug: TestConsoleCmd._debug(" - not an ack") + return + + # suck out the record data + if apdu.accessMethod.recordAccess: + value = apdu.accessMethod.recordAccess.fileRecordData + elif apdu.accessMethod.streamAccess: + value = apdu.accessMethod.streamAccess.fileData + if _debug: TestConsoleCmd._debug(" - value: %r", value) + + sys.stdout.write(repr(value) + '\n') + sys.stdout.flush() + + # do something for error/reject/abort + if iocb.ioError: + sys.stdout.write(str(iocb.ioError) + '\n') except Exception as error: TestConsoleCmd._exception("exception: %r", error) @@ -151,8 +135,38 @@ def do_readstream(self, args): request.pduDestination = Address(addr) if _debug: TestConsoleCmd._debug(" - request: %r", request) + # make an IOCB + iocb = IOCB(request) + if _debug: TestConsoleCmd._debug(" - iocb: %r", iocb) + # give it to the application - this_application.request(request) + this_application.request_io(iocb) + + # wait for it to complete + iocb.wait() + + # do something for success + if iocb.ioResponse: + apdu = iocb.ioResponse + + # should be an ack + if not isinstance(apdu, AtomicReadFileACK): + if _debug: TestConsoleCmd._debug(" - not an ack") + return + + # suck out the record data + if apdu.accessMethod.recordAccess: + value = apdu.accessMethod.recordAccess.fileRecordData + elif apdu.accessMethod.streamAccess: + value = apdu.accessMethod.streamAccess.fileData + if _debug: TestConsoleCmd._debug(" - value: %r", value) + + sys.stdout.write(repr(value) + '\n') + sys.stdout.flush() + + # do something for error/reject/abort + if iocb.ioError: + sys.stdout.write(str(iocb.ioError) + '\n') except Exception as error: TestConsoleCmd._exception("exception: %r", error) @@ -185,8 +199,38 @@ def do_writerecord(self, args): request.pduDestination = Address(addr) if _debug: TestConsoleCmd._debug(" - request: %r", request) + # make an IOCB + iocb = IOCB(request) + if _debug: TestConsoleCmd._debug(" - iocb: %r", iocb) + # give it to the application - this_application.request(request) + this_application.request_io(iocb) + + # wait for it to complete + iocb.wait() + + # do something for success + if iocb.ioResponse: + apdu = iocb.ioResponse + + # should be an ack + if not isinstance(apdu, AtomicWriteFileACK): + if _debug: TestConsoleCmd._debug(" - not an ack") + return + + # suck out the record data + if apdu.fileStartPosition is not None: + value = apdu.fileStartPosition + elif apdu.fileStartRecord is not None: + value = apdu.fileStartRecord + TestConsoleCmd._debug(" - value: %r", value) + + sys.stdout.write(repr(value) + '\n') + sys.stdout.flush() + + # do something for error/reject/abort + if iocb.ioError: + sys.stdout.write(str(iocb.ioError) + '\n') except Exception as error: TestConsoleCmd._exception("exception: %r", error) @@ -217,8 +261,38 @@ def do_writestream(self, args): request.pduDestination = Address(addr) if _debug: TestConsoleCmd._debug(" - request: %r", request) + # make an IOCB + iocb = IOCB(request) + if _debug: TestConsoleCmd._debug(" - iocb: %r", iocb) + # give it to the application - this_application.request(request) + this_application.request_io(iocb) + + # wait for it to complete + iocb.wait() + + # do something for success + if iocb.ioResponse: + apdu = iocb.ioResponse + + # should be an ack + if not isinstance(apdu, AtomicWriteFileACK): + if _debug: TestConsoleCmd._debug(" - not an ack") + return + + # suck out the record data + if apdu.fileStartPosition is not None: + value = apdu.fileStartPosition + elif apdu.fileStartRecord is not None: + value = apdu.fileStartRecord + TestConsoleCmd._debug(" - value: %r", value) + + sys.stdout.write(repr(value) + '\n') + sys.stdout.flush() + + # do something for error/reject/abort + if iocb.ioError: + sys.stdout.write(str(iocb.ioError) + '\n') except Exception as error: TestConsoleCmd._exception("exception: %r", error) @@ -246,7 +320,7 @@ def main(): ) # make a simple application - this_application = TestApplication(this_device, args.ini.address) + this_application = BIPSimpleApplication(this_device, args.ini.address) # get the services supported services_supported = this_application.get_services_supported() @@ -257,6 +331,7 @@ def main(): # make a console this_console = TestConsoleCmd() + if _debug: _log.debug(" - this_console: %r", this_console) # enable sleeping will help with threads enable_sleeping() diff --git a/samples/ReadWriteFileServer.py b/samples/ReadWriteFileServer.py index 7cc0fe6b..89df3375 100755 --- a/samples/ReadWriteFileServer.py +++ b/samples/ReadWriteFileServer.py @@ -14,8 +14,10 @@ from bacpypes.core import run -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication -from bacpypes.object import FileObject, register_object_type +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject +from bacpypes.service.file import FileServices, \ + LocalRecordAccessFileObject, LocalStreamAccessFileObject # some debugging _debug = 0 @@ -31,19 +33,17 @@ # @bacpypes_debugging -class LocalRecordAccessFileObject(FileObject): +class TestRecordFile(LocalRecordAccessFileObject): def __init__(self, **kwargs): """ Initialize a record accessed file object. """ if _debug: - LocalRecordAccessFileObject._debug("__init__ %r", + TestRecordFile._debug("__init__ %r", kwargs, ) - FileObject.__init__(self, - fileAccessMethod='recordAccess', - **kwargs - ) + LocalRecordAccessFileObject.__init__(self, **kwargs) + # create some test data self._record_data = [ ''.join(random.choice(string.ascii_letters) for i in range(RECORD_LEN)).encode('utf-8') @@ -55,13 +55,13 @@ def __init__(self, **kwargs): def __len__(self): """ Return the number of records. """ - if _debug: LocalRecordAccessFileObject._debug("__len__") + if _debug: TestRecordFile._debug("__len__") return len(self._record_data) - def ReadFile(self, start_record, record_count): + def read_record(self, start_record, record_count): """ Read a number of records starting at a specific record. """ - if _debug: LocalRecordAccessFileObject._debug("ReadFile %r %r", + if _debug: TestRecordFile._debug("read_record %r %r", start_record, record_count, ) @@ -71,9 +71,9 @@ def ReadFile(self, start_record, record_count): return end_of_file, \ self._record_data[start_record:start_record + record_count] - def WriteFile(self, start_record, record_count, record_data): + def write_record(self, start_record, record_count, record_data): """ Write a number of records, starting at a specific record. """ - if _debug: LocalRecordAccessFileObject._debug("WriteFile %r %r %r", + if _debug: TestRecordFile._debug("write_record %r %r %r", start_record, record_count, record_data, ) @@ -95,41 +95,37 @@ def WriteFile(self, start_record, record_count, record_data): # return where the 'writing' actually started return start_record -register_object_type(LocalRecordAccessFileObject) - # # Local Stream Access File Object Type # @bacpypes_debugging -class LocalStreamAccessFileObject(FileObject): +class TestStreamFile(LocalStreamAccessFileObject): def __init__(self, **kwargs): """ Initialize a stream accessed file object. """ if _debug: - LocalStreamAccessFileObject._debug("__init__ %r", + TestStreamFile._debug("__init__ %r", kwargs, ) - FileObject.__init__(self, - fileAccessMethod='streamAccess', - **kwargs - ) + LocalStreamAccessFileObject.__init__(self, **kwargs) + # create some test data self._file_data = ''.join(random.choice(string.ascii_letters) for i in range(OCTET_COUNT)).encode('utf-8') - if _debug: LocalRecordAccessFileObject._debug(" - %d octets", + if _debug: TestStreamFile._debug(" - %d octets", len(self._file_data), ) def __len__(self): """ Return the number of octets in the file. """ - if _debug: LocalStreamAccessFileObject._debug("__len__") + if _debug: TestStreamFile._debug("__len__") return len(self._file_data) - def ReadFile(self, start_position, octet_count): + def read_stream(self, start_position, octet_count): """ Read a chunk of data out of the file. """ - if _debug: LocalStreamAccessFileObject._debug("ReadFile %r %r", + if _debug: TestStreamFile._debug("read_stream %r %r", start_position, octet_count, ) @@ -139,9 +135,9 @@ def ReadFile(self, start_position, octet_count): return end_of_file, \ self._file_data[start_position:start_position + octet_count] - def WriteFile(self, start_position, data): + def write_stream(self, start_position, data): """ Write a number of octets, starting at a specific offset. """ - if _debug: LocalStreamAccessFileObject._debug("WriteFile %r %r", + if _debug: TestStreamFile._debug("write_stream %r %r", start_position, data, ) @@ -156,7 +152,7 @@ def WriteFile(self, start_position, data): start_position = len(self._file_data) self._file_data += data - # no slice assignment, strings are immutable + # no slice assignment, strings are immutable else: data_len = len(data) prechunk = self._file_data[:start_position] @@ -166,8 +162,6 @@ def WriteFile(self, start_position, data): # return where the 'writing' actually started return start_position -register_object_type(LocalStreamAccessFileObject) - # # __main__ # @@ -191,6 +185,9 @@ def main(): # make a sample application this_application = BIPSimpleApplication(this_device, args.ini.address) + # add the capability to server file content + this_application.add_capability(FileServices) + # get the services supported services_supported = this_application.get_services_supported() if _debug: _log.debug(" - services_supported: %r", services_supported) @@ -199,7 +196,7 @@ def main(): this_device.protocolServicesSupported = services_supported.value # make a record access file, add to the device - f1 = LocalRecordAccessFileObject( + f1 = TestRecordFile( objectIdentifier=('file', 1), objectName='RecordAccessFile1' ) @@ -207,7 +204,7 @@ def main(): this_application.add_object(f1) # make a stream access file, add to the device - f2 = LocalStreamAccessFileObject( + f2 = TestStreamFile( objectIdentifier=('file', 2), objectName='StreamAccessFile2' ) diff --git a/samples/ReadWriteProperty.py b/samples/ReadWriteProperty.py index a8e6aa8e..8c982f1d 100755 --- a/samples/ReadWriteProperty.py +++ b/samples/ReadWriteProperty.py @@ -15,16 +15,19 @@ from bacpypes.consolecmd import ConsoleCmd from bacpypes.core import run, enable_sleeping +from bacpypes.iocb import IOCB from bacpypes.pdu import Address -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication from bacpypes.object import get_object_class, get_datatype -from bacpypes.apdu import Error, AbortPDU, SimpleAckPDU, \ +from bacpypes.apdu import SimpleAckPDU, \ ReadPropertyRequest, ReadPropertyACK, WritePropertyRequest from bacpypes.primitivedata import Null, Atomic, Integer, Unsigned, Real from bacpypes.constructeddata import Array, Any +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -32,63 +35,6 @@ # globals this_application = None -# -# ReadPropertyApplication -# - -@bacpypes_debugging -class ReadPropertyApplication(BIPSimpleApplication): - - def __init__(self, *args): - if _debug: ReadPropertyApplication._debug("__init__ %r", args) - BIPSimpleApplication.__init__(self, *args) - - # keep track of requests to line up responses - self._request = None - - def request(self, apdu): - if _debug: ReadPropertyApplication._debug("request %r", apdu) - - # save a copy of the request - self._request = apdu - - # forward it along - BIPSimpleApplication.request(self, apdu) - - def confirmation(self, apdu): - if _debug: ReadPropertyApplication._debug("confirmation %r", apdu) - - if isinstance(apdu, Error): - sys.stdout.write("error: %s\n" % (apdu.errorCode,)) - sys.stdout.flush() - - elif isinstance(apdu, AbortPDU): - apdu.debug_contents() - - if isinstance(apdu, SimpleAckPDU): - sys.stdout.write("ack\n") - sys.stdout.flush() - - elif (isinstance(self._request, ReadPropertyRequest)) and (isinstance(apdu, ReadPropertyACK)): - # find the datatype - datatype = get_datatype(apdu.objectIdentifier[0], apdu.propertyIdentifier) - if _debug: ReadPropertyApplication._debug(" - datatype: %r", datatype) - if not datatype: - raise TypeError("unknown datatype") - - # special case for array parts, others are managed by cast_out - if issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): - if apdu.propertyArrayIndex == 0: - value = apdu.propertyValue.cast_out(Unsigned) - else: - value = apdu.propertyValue.cast_out(datatype.subtype) - else: - value = apdu.propertyValue.cast_out(datatype) - if _debug: ReadPropertyApplication._debug(" - value: %r", value) - - sys.stdout.write(str(value) + '\n') - sys.stdout.flush() - # # ReadWritePropertyConsoleCmd # @@ -126,8 +72,49 @@ def do_read(self, args): request.propertyArrayIndex = int(args[4]) if _debug: ReadWritePropertyConsoleCmd._debug(" - request: %r", request) + # make an IOCB + iocb = IOCB(request) + if _debug: ReadWritePropertyConsoleCmd._debug(" - iocb: %r", iocb) + # give it to the application - this_application.request(request) + this_application.request_io(iocb) + + # wait for it to complete + iocb.wait() + + # do something for success + if iocb.ioResponse: + apdu = iocb.ioResponse + + # should be an ack + if not isinstance(apdu, ReadPropertyACK): + if _debug: ReadWritePropertyConsoleCmd._debug(" - not an ack") + return + + # find the datatype + datatype = get_datatype(apdu.objectIdentifier[0], apdu.propertyIdentifier) + if _debug: ReadWritePropertyConsoleCmd._debug(" - datatype: %r", datatype) + if not datatype: + raise TypeError("unknown datatype") + + # special case for array parts, others are managed by cast_out + if issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): + if apdu.propertyArrayIndex == 0: + value = apdu.propertyValue.cast_out(Unsigned) + else: + value = apdu.propertyValue.cast_out(datatype.subtype) + else: + value = apdu.propertyValue.cast_out(datatype) + if _debug: ReadWritePropertyConsoleCmd._debug(" - value: %r", value) + + sys.stdout.write(str(value) + '\n') + if hasattr(value, 'debug_contents'): + value.debug_contents(file=sys.stdout) + sys.stdout.flush() + + # do something for error/reject/abort + if iocb.ioError: + sys.stdout.write(str(iocb.ioError) + '\n') except Exception as error: ReadWritePropertyConsoleCmd._exception("exception: %r", error) @@ -205,8 +192,28 @@ def do_write(self, args): if _debug: ReadWritePropertyConsoleCmd._debug(" - request: %r", request) + # make an IOCB + iocb = IOCB(request) + if _debug: ReadWritePropertyConsoleCmd._debug(" - iocb: %r", iocb) + # give it to the application - this_application.request(request) + this_application.request_io(iocb) + + # wait for it to complete + iocb.wait() + + # do something for success + if iocb.ioResponse: + # should be an ack + if not isinstance(iocb.ioResponse, SimpleAckPDU): + if _debug: ReadWritePropertyConsoleCmd._debug(" - not an ack") + return + + sys.stdout.write("ack\n") + + # do something for error/reject/abort + if iocb.ioError: + sys.stdout.write(str(iocb.ioError) + '\n') except Exception as error: ReadWritePropertyConsoleCmd._exception("exception: %r", error) @@ -251,7 +258,7 @@ def main(): ) # make a simple application - this_application = ReadPropertyApplication(this_device, args.ini.address) + this_application = BIPSimpleApplication(this_device, args.ini.address) # get the services supported services_supported = this_application.get_services_supported() @@ -262,6 +269,7 @@ def main(): # make a console this_console = ReadWritePropertyConsoleCmd() + if _debug: _log.debug(" - this_console: %r", this_console) # enable sleeping will help with threads enable_sleeping() diff --git a/samples/RecurringMultipleReadProperty.py b/samples/RecurringMultipleReadProperty.py index 785f2f55..7d4d8cbd 100644 --- a/samples/RecurringMultipleReadProperty.py +++ b/samples/RecurringMultipleReadProperty.py @@ -1,10 +1,10 @@ #!/usr/bin/env python """ -Mutliple Read Property +Recurring Read Property -This application has a static list of points that it would like to read. It reads the -values of each of them in turn and then quits. +This application has a static list of points that it would like to read. It +reads the values of each of them in turn and then quits. """ from collections import deque @@ -16,13 +16,15 @@ from bacpypes.task import RecurringTask from bacpypes.pdu import Address -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication from bacpypes.object import get_datatype from bacpypes.apdu import ReadPropertyRequest, Error, AbortPDU, ReadPropertyACK from bacpypes.primitivedata import Unsigned from bacpypes.constructeddata import Array +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -41,19 +43,14 @@ class PrairieDog(BIPSimpleApplication, RecurringTask): def __init__(self, interval, *args): - if _debug: PrairieDog._debug("__init__ %r, %r", interval, args) + if _debug: PrairieDog._debug("__init__ %r %r", interval, args) BIPSimpleApplication.__init__(self, *args) RecurringTask.__init__(self, interval * 1000) - # keep track of requests to line up responses - self._request = None - - # start out idle + # no longer busy self.is_busy = False - self.point_queue = deque() - self.response_values = [] - # install it + # install the task self.install_task() def process_task(self): @@ -97,28 +94,26 @@ def next_request(self): addr, obj_type, obj_inst, prop_id = self.point_queue.popleft() # build a request - self._request = ReadPropertyRequest( + request = ReadPropertyRequest( objectIdentifier=(obj_type, obj_inst), propertyIdentifier=prop_id, ) - self._request.pduDestination = Address(addr) - if _debug: PrairieDog._debug(" - request: %r", self._request) + request.pduDestination = Address(addr) + if _debug: PrairieDog._debug(" - request: %r", request) - # forward it along - BIPSimpleApplication.request(self, self._request) + # send the request + iocb = self.request(request) + if _debug: PrairieDog._debug(" - iocb: %r", iocb) - def confirmation(self, apdu): - if _debug: PrairieDog._debug("confirmation %r", apdu) + # set a callback for the response + iocb.add_callback(self.complete_request) - if isinstance(apdu, Error): - if _debug: PrairieDog._debug(" - error: %r", apdu) - self.response_values.append(apdu) + def complete_request(self, iocb): + if _debug: PrairieDog._debug("complete_request %r", iocb) - elif isinstance(apdu, AbortPDU): - if _debug: PrairieDog._debug(" - abort: %r", apdu) - self.response_values.append(apdu) + if iocb.ioResponse: + apdu = iocb.ioResponse - elif (isinstance(self._request, ReadPropertyRequest)) and (isinstance(apdu, ReadPropertyACK)): # find the datatype datatype = get_datatype(apdu.objectIdentifier[0], apdu.propertyIdentifier) if _debug: PrairieDog._debug(" - datatype: %r", datatype) @@ -138,6 +133,10 @@ def confirmation(self, apdu): # save the value self.response_values.append(value) + if iocb.ioError: + if _debug: PrairieDog._debug(" - error: %r", iocb.ioError) + self.response_values.append(iocb.ioError) + # fire off another request deferred(self.next_request) diff --git a/samples/RecurringTask.py b/samples/RecurringTask.py index b9fc6ba2..24b2a7ce 100755 --- a/samples/RecurringTask.py +++ b/samples/RecurringTask.py @@ -25,13 +25,11 @@ class PrairieDog(RecurringTask): def __init__(self, dog_number, interval): if _debug: PrairieDog._debug("__init__ %r %r", dog_number, interval) + RecurringTask.__init__(self, interval) # save the identity self.dog_number = dog_number - # this is a recurring task - RecurringTask.__init__(self, interval) - # install it self.install_task() diff --git a/samples/TCPClient.py b/samples/TCPClient.py index 7a524f0e..b86355d8 100644 --- a/samples/TCPClient.py +++ b/samples/TCPClient.py @@ -8,9 +8,9 @@ import os -from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob -from bacpypes.core import run, stop +from bacpypes.core import run, stop, deferred from bacpypes.task import TaskManager from bacpypes.comm import PDU, Client, Server, bind, ApplicationServiceElement @@ -33,7 +33,6 @@ # MiddleMan # -@bacpypes_debugging class MiddleMan(Client, Server): """ An instance of this class sits between the TCPClientDirector and the @@ -57,36 +56,47 @@ def indication(self, pdu): def confirmation(self, pdu): if _debug: MiddleMan._debug("confirmation %r", pdu) + # check for errors + if isinstance(pdu, Exception): + if _debug: MiddleMan._debug(" - exception: %s", pdu) + return + # pass it along self.response(pdu) +bacpypes_debugging(MiddleMan) # # MiddleManASE # -@bacpypes_debugging class MiddleManASE(ApplicationServiceElement): + """ + An instance of this class is bound to the director, which is a + ServiceAccessPoint. It receives notifications of new actors connected + to a server, actors that are going away when the connections are closed, + and socket errors. + """ + def indication(self, add_actor=None, del_actor=None, actor_error=None, error=None): + if add_actor: + if _debug: MiddleManASE._debug("indication add_actor=%r", add_actor) - def indication(self, addPeer=None, delPeer=None): - """ - This function is called by the TCPDirector when the client connects to - or disconnects from a server. It is called with addPeer or delPeer - keyword parameters, but not both. - """ - if _debug: MiddleManASE._debug('indication addPeer=%r delPeer=%r', addPeer, delPeer) - - if addPeer: - if _debug: MiddleManASE._debug(" - add peer %s", addPeer) + if del_actor: + if _debug: MiddleManASE._debug("indication del_actor=%r", del_actor) - if delPeer: - if _debug: MiddleManASE._debug(" - delete peer %s", delPeer) + if actor_error: + if _debug: MiddleManASE._debug("indication actor_error=%r error=%r", actor_error, error) # if there are no clients, quit if not self.elementService.clients: if _debug: MiddleManASE._debug(" - quitting") stop() +bacpypes_debugging(MiddleManASE) + +# +# main +# def main(): """ @@ -98,14 +108,19 @@ def main(): parser = ArgumentParser(description=__doc__) parser.add_argument( "host", nargs='?', - help="address of host (default {!r})".format(SERVER_HOST), + help="address of host (default %r)" % (SERVER_HOST,), default=SERVER_HOST, ) parser.add_argument( "port", nargs='?', type=int, - help="server port (default {!r})".format(SERVER_PORT), + help="server port (default %r)" % (SERVER_PORT,), default=SERVER_PORT, ) + parser.add_argument( + "--hello", action="store_true", + default=False, + help="send a hello message", + ) args = parser.parse_args() if _debug: _log.debug("initialization") @@ -135,12 +150,17 @@ def main(): if _debug: _log.debug(" - task_manager: %r", task_manager) # don't wait to connect - this_director.connect(server_address) + deferred(this_director.connect, server_address) + + # send hello maybe + if args.hello: + deferred(this_middle_man.indication, PDU(xtob('68656c6c6f0a'))) if _debug: _log.debug("running") run() + if _debug: _log.debug("fini") if __name__ == "__main__": main() diff --git a/samples/TCPClient25.py b/samples/TCPClient25.py new file mode 100755 index 00000000..458fe48a --- /dev/null +++ b/samples/TCPClient25.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python + +""" +This simple TCP client application connects to a server and sends the text +entered in the console. There is no conversion from incoming streams of +content into a line or any other higher-layer concept of a packet. +""" + +import os +import sys + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger + +from bacpypes.core import run, stop +from bacpypes.task import TaskManager +from bacpypes.comm import PDU, Client, Server, bind, ApplicationServiceElement + +from bacpypes.consolelogging import ArgumentParser +from bacpypes.console import ConsoleClient +from bacpypes.tcp import TCPClientDirector + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# settings +SERVER_HOST = os.getenv('SERVER_HOST', '127.0.0.1') +SERVER_PORT = int(os.getenv('SERVER_PORT', 9000)) + +# globals +server_address = None + +# +# MiddleMan +# + +class MiddleMan(Client, Server): + """ + An instance of this class sits between the TCPClientDirector and the + console client. Downstream packets from a console have no concept of a + destination, so this is added to the PDUs before being sent to the + director. The source information in upstream packets is ignored by the + console client. + """ + def indication(self, pdu): + if _debug: MiddleMan._debug("indication %r", pdu) + global server_address + + # no data means EOF, stop + if not pdu.pduData: + stop() + return + + # pass it along + self.request(PDU(pdu.pduData, destination=server_address)) + + def confirmation(self, pdu): + if _debug: MiddleMan._debug("confirmation %r", pdu) + + # pass it along + self.response(pdu) + +bacpypes_debugging(MiddleMan) + +# +# MiddleManASE +# + +class MiddleManASE(ApplicationServiceElement): + + def indication(self, add_actor=None, del_actor=None, actor_error=None, error=None): + if add_actor: + if _debug: MiddleManASE._debug("indication add_actor=%r", add_actor) + + if del_actor: + if _debug: MiddleManASE._debug("indication del_actor=%r", del_actor) + + if actor_error: + if _debug: MiddleManASE._debug("indication actor_error=%r error=%r", actor_error, error) + + # if there are no clients, quit + if not self.elementService.clients: + if _debug: MiddleManASE._debug(" - quitting") + stop() + +bacpypes_debugging(MiddleManASE) + +def main(): + """ + Main function, called when run as an application. + """ + global server_address + + # check the version + if (sys.version_info[:2] != (2, 5)): + sys.stderr.write("Python 2.5 only\n") + sys.exit(1) + + # parse the command line arguments + parser = ArgumentParser(description=__doc__) + parser.add_argument( + "host", nargs='?', + help="address of host (default %r)" % (SERVER_HOST,), + default=SERVER_HOST, + ) + parser.add_argument( + "port", nargs='?', type=int, + help="server port (default %r)" % (SERVER_PORT,), + default=SERVER_PORT, + ) + args = parser.parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # extract the server address and port + host = args.host + port = args.port + server_address = (host, port) + if _debug: _log.debug(" - server_address: %r", server_address) + + # build the stack + this_console = ConsoleClient() + if _debug: _log.debug(" - this_console: %r", this_console) + + this_middle_man = MiddleMan() + if _debug: _log.debug(" - this_middle_man: %r", this_middle_man) + + this_director = TCPClientDirector() + if _debug: _log.debug(" - this_director: %r", this_director) + + bind(this_console, this_middle_man, this_director) + bind(MiddleManASE(), this_director) + + # create a task manager for scheduled functions + task_manager = TaskManager() + if _debug: _log.debug(" - task_manager: %r", task_manager) + + # don't wait to connect + this_director.connect(server_address) + + if _debug: _log.debug("running") + + run() + + +if __name__ == "__main__": + main() diff --git a/samples/TCPServer.py b/samples/TCPServer.py index b020e42a..baa469a5 100755 --- a/samples/TCPServer.py +++ b/samples/TCPServer.py @@ -2,7 +2,7 @@ """ This simple TCP server application listens for one or more client connections -and echos the incoming lines back to the client. There is no conversion from +and echos the incoming lines back to the client. There is no conversion from incoming streams of content into a line or any other higher-layer concept of a packet. """ @@ -28,7 +28,6 @@ # EchoMaster # -@bacpypes_debugging class EchoMaster(Client): def confirmation(self, pdu): @@ -37,28 +36,30 @@ def confirmation(self, pdu): # send it back down the stack self.request(PDU(pdu.pduData, destination=pdu.pduSource)) +bacpypes_debugging(EchoMaster) # # MiddleManASE # -@bacpypes_debugging class MiddleManASE(ApplicationServiceElement): + """ + An instance of this class is bound to the director, which is a + ServiceAccessPoint. It receives notifications of new actors connected + from a client, actors that are going away when the connections are closed, + and socket errors. + """ + def indication(self, add_actor=None, del_actor=None, actor_error=None, error=None): + if add_actor: + if _debug: MiddleManASE._debug("indication add_actor=%r", add_actor) - def indication(self, addPeer=None, delPeer=None): - """ - This function is called by the TCPDirector when the client connects to - or disconnects from a server. It is called with addPeer or delPeer - keyword parameters, but not both. - """ - if _debug: MiddleManASE._debug('indication addPeer=%r delPeer=%r', addPeer, delPeer) + if del_actor: + if _debug: MiddleManASE._debug("indication del_actor=%r", del_actor) - if addPeer: - if _debug: MiddleManASE._debug(" - add peer %s", addPeer) - - if delPeer: - if _debug: MiddleManASE._debug(" - delete peer %s", delPeer) + if actor_error: + if _debug: MiddleManASE._debug("indication actor_error=%r error=%r", actor_error, error) +bacpypes_debugging(MiddleManASE) # # __main__ @@ -69,12 +70,12 @@ def main(): parser = ArgumentParser(description=__doc__) parser.add_argument( "host", nargs='?', - help="listening address of server or 'any' (default {!r})".format(SERVER_HOST), + help="listening address of server or 'any' (default %r)" % (SERVER_HOST,), default=SERVER_HOST, ) parser.add_argument( "port", nargs='?', type=int, - help="server port (default {!r})".format(SERVER_PORT), + help="server port (default %r)" % (SERVER_PORT,), default=SERVER_PORT, ) args = parser.parse_args() diff --git a/samples/TCPServer25.py b/samples/TCPServer25.py new file mode 100755 index 00000000..604aeb3c --- /dev/null +++ b/samples/TCPServer25.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python + +""" +This simple TCP server application listens for one or more client connections +and echos the incoming lines back to the client. There is no conversion from +incoming streams of content into a line or any other higher-layer concept +of a packet. +""" + +import os +import sys + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ArgumentParser + +from bacpypes.core import run +from bacpypes.comm import PDU, Client, bind, ApplicationServiceElement +from bacpypes.tcp import TCPServerDirector + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# settings +SERVER_HOST = os.getenv('SERVER_HOST', 'any') +SERVER_PORT = int(os.getenv('SERVER_PORT', 9000)) + +# +# EchoMaster +# + +class EchoMaster(Client): + + def confirmation(self, pdu): + if _debug: EchoMaster._debug('confirmation %r', pdu) + + # send it back down the stack + self.request(PDU(pdu.pduData, destination=pdu.pduSource)) + +bacpypes_debugging(EchoMaster) + +# +# MiddleManASE +# + +class MiddleManASE(ApplicationServiceElement): + + def indication(self, add_actor=None, del_actor=None, actor_error=None, error=None): + if add_actor: + if _debug: MiddleManASE._debug("indication add_actor=%r", add_actor) + + if del_actor: + if _debug: MiddleManASE._debug("indication del_actor=%r", del_actor) + + if actor_error: + if _debug: MiddleManASE._debug("indication actor_error=%r error=%r", actor_error, error) + +bacpypes_debugging(MiddleManASE) + +# +# __main__ +# + +def main(): + # check the version + if (sys.version_info[:2] != (2, 5)): + sys.stderr.write("Python 2.5 only\n") + sys.exit(1) + + # parse the command line arguments + parser = ArgumentParser(description=__doc__) + parser.add_argument( + "host", nargs='?', + help="listening address of server or 'any' (default %r)" % (SERVER_HOST,), + default=SERVER_HOST, + ) + parser.add_argument( + "port", nargs='?', type=int, + help="server port (default %r)" % (SERVER_PORT,), + default=SERVER_PORT, + ) + args = parser.parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # extract the server address and port + host = args.host + if host == "any": + host = '' + server_address = (host, args.port) + if _debug: _log.debug(" - server_address: %r", server_address) + + # create a director listening to the address + this_director = TCPServerDirector(server_address) + if _debug: _log.debug(" - this_director: %r", this_director) + + # create an echo + echo_master = EchoMaster() + if _debug: _log.debug(" - echo_master: %r", echo_master) + + # bind everything together + bind(echo_master, this_director) + bind(MiddleManASE(), this_director) + + _log.debug("running") + + run() + + _log.debug("fini") + + +if __name__ == "__main__": + main() diff --git a/samples/Tutorial/Capabilities.py b/samples/Tutorial/Capabilities.py new file mode 100644 index 00000000..7dba1100 --- /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 00000000..407ee14c --- /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 00000000..fe8a7579 --- /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/Tutorial/SampleConsoleCmd-Answer.py b/samples/Tutorial/SampleConsoleCmd-Answer.py new file mode 100644 index 00000000..4198f548 --- /dev/null +++ b/samples/Tutorial/SampleConsoleCmd-Answer.py @@ -0,0 +1,135 @@ +#!/usr/bin/env python + +""" +This sample application is a simple BACpypes application that +presents a console prompt. Almost identical to the SampleApplication, +the BACnet application is minimal, but with the console commands +that match the command line options like 'buggers' and 'debug' the +user can add debugging "on the fly". +""" + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ConfigArgumentParser +from bacpypes.consolecmd import ConsoleCmd + +from bacpypes.core import run, enable_sleeping + +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# 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) + + def request(self, apdu): + if _debug: SampleApplication._debug("request %r", apdu) + BIPSimpleApplication.request(self, apdu) + + def indication(self, apdu): + if _debug: SampleApplication._debug("indication %r", apdu) + BIPSimpleApplication.indication(self, apdu) + + def response(self, apdu): + if _debug: SampleApplication._debug("response %r", apdu) + BIPSimpleApplication.response(self, apdu) + + def confirmation(self, apdu): + if _debug: SampleApplication._debug("confirmation %r", apdu) + BIPSimpleApplication.confirmation(self, apdu) + + +# +# SampleConsoleCmd +# + +@bacpypes_debugging +class SampleConsoleCmd(ConsoleCmd): + + my_cache= {} + + def do_set(self, arg): + """set - change a cache value""" + if _debug: SampleConsoleCmd._debug("do_set %r", arg) + + key, value = arg.split() + self.my_cache[key] = value + + def do_del(self, arg): + """del - delete a cache entry""" + if _debug: SampleConsoleCmd._debug("do_del %r", arg) + + try: + del self.my_cache[arg] + except: + print(arg, "not in cache") + + def do_dump(self, arg): + """dump - nicely print the cache""" + if _debug: SampleConsoleCmd._debug("do_dump %r", arg) + print(self.my_cache) + + def do_something(self, arg): + """something - do something""" + print("do something", arg) + + def do_nothing(self, args): + """nothing can be done""" + args = args.split() + if _debug: SampleConsoleCmd._debug("do_nothing %r", args) + + +# +# __main__ +# + +def main(): + # parse the command line arguments + args = ConfigArgumentParser(description=__doc__).parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # 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), + ) + + # make a sample application + this_application = SampleApplication(this_device, args.ini.address) + + # 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 + + # make a console + this_console = SampleConsoleCmd() + + # enable sleeping will help with threads + enable_sleeping() + + _log.debug("running") + + run() + + _log.debug("fini") + +if __name__ == "__main__": + main() diff --git a/samples/SampleConsoleCmd.py b/samples/Tutorial/SampleConsoleCmd.py old mode 100755 new mode 100644 similarity index 94% rename from samples/SampleConsoleCmd.py rename to samples/Tutorial/SampleConsoleCmd.py index a0d733fc..37e34c6f --- a/samples/SampleConsoleCmd.py +++ b/samples/Tutorial/SampleConsoleCmd.py @@ -14,7 +14,8 @@ from bacpypes.core import run, enable_sleeping -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject # some debugging _debug = 0 @@ -59,8 +60,7 @@ def do_nothing(self, args): """nothing can be done""" args = args.split() if _debug: SampleConsoleCmd._debug("do_nothing %r", args) - - + # # __main__ # @@ -93,6 +93,7 @@ def main(): # make a console this_console = SampleConsoleCmd() + if _debug: _log.debug(" - this_console: %r", this_console) # enable sleeping will help with threads enable_sleeping() diff --git a/samples/WhoIsIAm.py b/samples/Tutorial/WhoIsIAm.py old mode 100755 new mode 100644 similarity index 80% rename from samples/WhoIsIAm.py rename to samples/Tutorial/WhoIsIAm.py index c00803b2..b65e9669 --- a/samples/WhoIsIAm.py +++ b/samples/Tutorial/WhoIsIAm.py @@ -2,7 +2,7 @@ """ This application presents a 'console' prompt to the user asking for Who-Is and I-Am -commands which create the related APDUs, then lines up the coorresponding I-Am +commands which create the related APDUs, then lines up the corresponding I-Am for incoming traffic and prints out the contents. """ @@ -15,17 +15,19 @@ from bacpypes.core import run, enable_sleeping from bacpypes.pdu import Address, GlobalBroadcast -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication - from bacpypes.apdu import WhoIsRequest, IAmRequest from bacpypes.basetypes import ServicesSupported from bacpypes.errors import DecodingError +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject + # some debugging -_debug = 0 +_debug = 1 _log = ModuleLogger(globals()) # globals +this_device = None this_application = None # @@ -92,26 +94,27 @@ def indication(self, apdu): class WhoIsIAmConsoleCmd(ConsoleCmd): def do_whois(self, args): - """whois [ ] [ ]""" + """whois [ ] [ ]""" args = args.split() if _debug: WhoIsIAmConsoleCmd._debug("do_whois %r", args) try: - # build a request + # gather the parameters request = WhoIsRequest() if (len(args) == 1) or (len(args) == 3): - request.pduDestination = Address(args[0]) + addr = Address(args[0]) del args[0] else: - request.pduDestination = GlobalBroadcast() + addr = GlobalBroadcast() if len(args) == 2: - request.deviceInstanceRangeLowLimit = int(args[0]) - request.deviceInstanceRangeHighLimit = int(args[1]) - if _debug: WhoIsIAmConsoleCmd._debug(" - request: %r", request) + lolimit = int(args[0]) + hilimit = int(args[1]) + else: + lolimit = hilimit = None - # give it to the application - this_application.request(request) + # code lives in the device service + this_application.who_is(lolimit, hilimit, addr) except Exception as error: WhoIsIAmConsoleCmd._exception("exception: %r", error) @@ -121,23 +124,8 @@ def do_iam(self, args): args = args.split() if _debug: WhoIsIAmConsoleCmd._debug("do_iam %r", args) - try: - # build a request - request = IAmRequest() - request.pduDestination = GlobalBroadcast() - - # set the parameters from the device object - request.iAmDeviceIdentifier = this_device.objectIdentifier - request.maxAPDULengthAccepted = this_device.maxApduLengthAccepted - request.segmentationSupported = this_device.segmentationSupported - request.vendorID = this_device.vendorIdentifier - if _debug: WhoIsIAmConsoleCmd._debug(" - request: %r", request) - - # give it to the application - this_application.request(request) - - except Exception as error: - WhoIsIAmConsoleCmd._exception("exception: %r", error) + # code lives in the device service + this_application.i_am() def do_rtn(self, args): """rtn ... """ @@ -161,6 +149,7 @@ def do_rtn(self, args): # def main(): + global this_device global this_application # parse the command line arguments diff --git a/samples/UDPConsole.py b/samples/UDPConsole.py index ca4912e2..ee2f183b 100755 --- a/samples/UDPConsole.py +++ b/samples/UDPConsole.py @@ -20,7 +20,7 @@ any - special tuple ('', 47808) any:12345 - special tuple ('', 12345) -Use the --nobroadcast option to prevent the application from opening the +Use the --nobroadcast option to prevent the application from opening the broadcast socket when one would otherwise be opened. To send a packet, enter in a string in the form where @@ -95,15 +95,25 @@ @bacpypes_debugging class MiddleMan(Client, Server): + """ + An instance of this class sits between the UDPDirector and the + console. Downstream packets from a console have no concept of a + destination, so this is interpreted from the text and then a new + PDU is sent to the director. Upstream packets could be simply + forwarded to the console, in that case the source address is ignored, + this application interprets the source address for the user. + """ + def indication(self, pdu): if _debug: MiddleMan._debug('indication %r', pdu) + # empty downstream packets mean EOF if not pdu.pduData: stop() return # decode the line and trim off the eol - line = pdu.pduData.decode('utf_8')[:-1] + line = pdu.pduData.decode('utf-8')[:-1] if _debug: MiddleMan._debug(' - line: %r', line) line_parts = line.split(' ', 1) @@ -144,9 +154,7 @@ def confirmation(self, pdu): if pdu.pduSource == local_unicast_tuple: sys.stdout.write("received %r from self\n" % (line,)) else: - sys.stdout.write("received %r from %s\n" % ( - line, pdu.pduSource, - )) + sys.stdout.write("received %r from %s\n" % (line, pdu.pduSource)) # @@ -156,19 +164,23 @@ def confirmation(self, pdu): @bacpypes_debugging class BroadcastReceiver(Client): + """ + An instance of this class sits above the UDPDirector that is + associated with the broadcast address. There are no downstream + packets, and it interprets the source address for the user. + """ + def confirmation(self, pdu): if _debug: BroadcastReceiver._debug('confirmation %r', pdu) # decode the line - line = pdu.pduData.decode('utf_8') + line = pdu.pduData.decode('utf-8') if _debug: MiddleMan._debug(' - line: %r', line) if pdu.pduSource == local_unicast_tuple: sys.stdout.write("received broadcast %r from self\n" % (line,)) else: - sys.stdout.write("received broadcast %r from %s\n" % ( - line, pdu.pduSource, - )) + sys.stdout.write("received broadcast %r from %s\n" % (line, pdu.pduSource,)) # @@ -176,8 +188,10 @@ def confirmation(self, pdu): # def main(): + global local_unicast_tuple, local_broadcast_tuple + # parse the command line arguments - parser = ArgumentParser(description=__doc__) + parser = ArgumentParser(usage=__doc__) parser.add_argument("address", help="address of socket", ) diff --git a/samples/VendorAVObject.py b/samples/VendorAVObject.py index fdc8fdc3..edef1e76 100755 --- a/samples/VendorAVObject.py +++ b/samples/VendorAVObject.py @@ -16,10 +16,12 @@ from bacpypes.core import run from bacpypes.primitivedata import Real -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication from bacpypes.object import AnalogValueObject, Property, register_object_type from bacpypes.errors import ExecutionError +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject + # some debugging _debug = 0 _log = ModuleLogger(globals()) diff --git a/samples/VendorReadWriteProperty.py b/samples/VendorReadWriteProperty.py index 75802b03..42916b9d 100755 --- a/samples/VendorReadWriteProperty.py +++ b/samples/VendorReadWriteProperty.py @@ -15,16 +15,19 @@ from bacpypes.consolecmd import ConsoleCmd from bacpypes.core import run, enable_sleeping +from bacpypes.iocb import IOCB from bacpypes.pdu import Address -from bacpypes.app import LocalDeviceObject, BIPSimpleApplication from bacpypes.object import get_object_class, get_datatype from bacpypes.apdu import Error, AbortPDU, SimpleAckPDU, \ ReadPropertyRequest, ReadPropertyACK, WritePropertyRequest -from bacpypes.primitivedata import Null, Atomic, Integer, Unsigned, Real +from bacpypes.primitivedata import Tag, Null, Atomic, Integer, Unsigned, Real from bacpypes.constructeddata import Array, Any +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject + import VendorAVObject # some debugging @@ -34,63 +37,6 @@ # globals this_application = None -# -# ReadPropertyApplication -# - -@bacpypes_debugging -class ReadPropertyApplication(BIPSimpleApplication): - - def __init__(self, *args): - if _debug: ReadPropertyApplication._debug("__init__ %r", args) - BIPSimpleApplication.__init__(self, *args) - - # keep track of requests to line up responses - self._request = None - - def request(self, apdu): - if _debug: ReadPropertyApplication._debug("request %r", apdu) - - # save a copy of the request - self._request = apdu - - # forward it along - BIPSimpleApplication.request(self, apdu) - - def confirmation(self, apdu): - if _debug: ReadPropertyApplication._debug("confirmation %r", apdu) - - if isinstance(apdu, Error): - sys.stdout.write("error: %s\n" % (apdu.errorCode,)) - sys.stdout.flush() - - elif isinstance(apdu, AbortPDU): - apdu.debug_contents() - - if isinstance(apdu, SimpleAckPDU): - sys.stdout.write("ack\n") - sys.stdout.flush() - - elif (isinstance(self._request, ReadPropertyRequest)) and (isinstance(apdu, ReadPropertyACK)): - # find the datatype - datatype = get_datatype(apdu.objectIdentifier[0], apdu.propertyIdentifier, VendorAVObject.vendor_id) - if _debug: ReadPropertyApplication._debug(" - datatype: %r", datatype) - if not datatype: - raise TypeError("unknown datatype") - - # special case for array parts, others are managed by cast_out - if issubclass(datatype, Array) and (apdu.propertyArrayIndex is not None): - if apdu.propertyArrayIndex == 0: - value = apdu.propertyValue.cast_out(Unsigned) - else: - value = apdu.propertyValue.cast_out(datatype.subtype) - else: - value = apdu.propertyValue.cast_out(datatype) - if _debug: ReadPropertyApplication._debug(" - value: %r", value) - - sys.stdout.write(str(value) + '\n') - sys.stdout.flush() - # # ReadWritePropertyConsoleCmd # @@ -134,8 +80,46 @@ def do_read(self, args): request.propertyArrayIndex = int(args[4]) if _debug: ReadWritePropertyConsoleCmd._debug(" - request: %r", request) + # make an IOCB + iocb = IOCB(request) + if _debug: ReadWritePropertyConsoleCmd._debug(" - iocb: %r", iocb) + # give it to the application - this_application.request(request) + this_application.request_io(iocb) + + # wait for it to complete + iocb.wait() + + # do something for success + if iocb.ioResponse: + apdu = iocb.ioResponse + + # peek at the value tag + value_tag = apdu.propertyValue.tagList.Peek() + if _debug: ReadWritePropertyConsoleCmd._debug(" - value_tag: %r", value_tag) + + # make sure that it is application tagged + if value_tag.tagClass != Tag.applicationTagClass: + sys.stdout.write("value is not application encoded\n") + + else: + # find the datatype + datatype = Tag._app_tag_class[value_tag.tagNumber] + if _debug: ReadWritePropertyConsoleCmd._debug(" - datatype: %r", datatype) + if not datatype: + raise TypeError("unknown datatype") + + # cast out the value + value = apdu.propertyValue.cast_out(datatype) + if _debug: ReadWritePropertyConsoleCmd._debug(" - value: %r", value) + + sys.stdout.write("%s (%s)\n" % (value, datatype)) + + sys.stdout.flush() + + # do something for error/reject/abort + if iocb.ioError: + sys.stdout.write(str(iocb.ioError) + '\n') except Exception as error: ReadWritePropertyConsoleCmd._exception("exception: %r", error) @@ -224,8 +208,23 @@ def do_write(self, args): if _debug: ReadWritePropertyConsoleCmd._debug(" - request: %r", request) + # make an IOCB + iocb = IOCB(request) + if _debug: ReadWritePropertyConsoleCmd._debug(" - iocb: %r", iocb) + # give it to the application - this_application.request(request) + this_application.request_io(iocb) + + # wait for it to complete + iocb.wait() + + # do something for success + if iocb.ioResponse: + sys.stdout.write("ack\n") + + # do something for error/reject/abort + if iocb.ioError: + sys.stdout.write(str(iocb.ioError) + '\n') except Exception as error: ReadWritePropertyConsoleCmd._exception("exception: %r", error) @@ -253,7 +252,7 @@ def main(): ) # make a simple application - this_application = ReadPropertyApplication(this_device, args.ini.address) + this_application = BIPSimpleApplication(this_device, args.ini.address) # get the services supported services_supported = this_application.get_services_supported() diff --git a/samples/WhoIsIAmForeign.py b/samples/WhoIsIAmForeign.py index 2e3b9e1d..38e2917a 100755 --- a/samples/WhoIsIAmForeign.py +++ b/samples/WhoIsIAmForeign.py @@ -13,14 +13,16 @@ from bacpypes.consolecmd import ConsoleCmd from bacpypes.core import run, enable_sleeping +from bacpypes.iocb import IOCB from bacpypes.pdu import Address, GlobalBroadcast -from bacpypes.app import LocalDeviceObject, BIPForeignApplication - from bacpypes.apdu import WhoIsRequest, IAmRequest from bacpypes.basetypes import ServicesSupported from bacpypes.errors import DecodingError +from bacpypes.app import BIPForeignApplication +from bacpypes.service.device import LocalDeviceObject + # some debugging _debug = 0 _log = ModuleLogger(globals()) @@ -111,8 +113,12 @@ def do_whois(self, args): request.deviceInstanceRangeHighLimit = int(args[1]) if _debug: WhoIsIAmConsoleCmd._debug(" - request: %r", request) + # make an IOCB + iocb = IOCB(request) + if _debug: WriteSomethingConsoleCmd._debug(" - iocb: %r", iocb) + # give it to the application - this_application.request(request) + this_application.request_io(iocb) except Exception as err: WhoIsIAmConsoleCmd._exception("exception: %r", err) @@ -134,8 +140,12 @@ def do_iam(self, args): request.vendorID = this_device.vendorIdentifier if _debug: WhoIsIAmConsoleCmd._debug(" - request: %r", request) + # make an IOCB + iocb = IOCB(request) + if _debug: WriteSomethingConsoleCmd._debug(" - iocb: %r", iocb) + # give it to the application - this_application.request(request) + this_application.request_io(iocb) except Exception as err: WhoIsIAmConsoleCmd._exception("exception: %r", err) diff --git a/samples/WhoIsRouter.py b/samples/WhoIsRouter.py index af33767d..10fd8cfc 100755 --- a/samples/WhoIsRouter.py +++ b/samples/WhoIsRouter.py @@ -13,6 +13,7 @@ from bacpypes.pdu import Address from bacpypes.npdu import InitializeRoutingTable, WhoIsRouterToNetwork + from bacpypes.app import BIPNetworkApplication # some debugging @@ -34,16 +35,8 @@ def __init__(self, *args): if _debug: WhoIsRouterApplication._debug("__init__ %r", args) BIPNetworkApplication.__init__(self, *args) - # keep track of requests to line up responses - self._request = None - def request(self, adapter, npdu): if _debug: WhoIsRouterApplication._debug("request %r %r", adapter, npdu) - - # save a copy of the request - self._request = npdu - - # forward it along BIPNetworkApplication.request(self, adapter, npdu) def indication(self, adapter, npdu): diff --git a/samples/WriteSomething.py b/samples/WriteSomething.py new file mode 100755 index 00000000..9f623aba --- /dev/null +++ b/samples/WriteSomething.py @@ -0,0 +1,154 @@ +#!/usr/bin/env python + +""" +This application is a special example of building a custom data structure +to be written to a proprietary property of a proprietary object. Unlike the +other 'write property' sample applications, this one make no attempt to +translate keywords into object types and property identifiers, it only takes +integers. +""" + +import sys + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger, xtob +from bacpypes.consolelogging import ConfigArgumentParser +from bacpypes.consolecmd import ConsoleCmd + +from bacpypes.core import run, enable_sleeping +from bacpypes.iocb import IOCB + +from bacpypes.pdu import Address +from bacpypes.app import BIPSimpleApplication +from bacpypes.service.device import LocalDeviceObject + +from bacpypes.primitivedata import TagList, OpeningTag, ClosingTag, ContextTag +from bacpypes.constructeddata import Any +from bacpypes.apdu import WritePropertyRequest, SimpleAckPDU + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# globals +this_application = None + +# +# WriteSomethingConsoleCmd +# + +@bacpypes_debugging +class WriteSomethingConsoleCmd(ConsoleCmd): + + def do_write(self, args): + """write [ ]""" + args = args.split() + if _debug: WriteSomethingConsoleCmd._debug("do_write %r", args) + + try: + addr, obj_type, obj_inst, prop_id = args[:4] + + obj_type = int(obj_type) + obj_inst = int(obj_inst) + prop_id = int(prop_id) + + # build a request + request = WritePropertyRequest( + objectIdentifier=(obj_type, obj_inst), + propertyIdentifier=prop_id, + ) + request.pduDestination = Address(addr) + + if len(args) == 5: + request.propertyArrayIndex = int(args[4]) + + # build a custom datastructure + tag_list = TagList([ + OpeningTag(1), + ContextTag(0, xtob('9c40')), + ContextTag(1, xtob('02')), + ContextTag(2, xtob('02')), + ClosingTag(1) + ]) + if _debug: WriteSomethingConsoleCmd._debug(" - tag_list: %r", tag_list) + + # stuff the tag list into an Any + request.propertyValue = Any() + request.propertyValue.decode(tag_list) + + if _debug: WriteSomethingConsoleCmd._debug(" - request: %r", request) + + # make an IOCB + iocb = IOCB(request) + if _debug: WriteSomethingConsoleCmd._debug(" - iocb: %r", iocb) + + # give it to the application + this_application.request_io(iocb) + + # wait for it to complete + iocb.wait() + + # do something for success + if iocb.ioResponse: + # should be an ack + if not isinstance(iocb.ioResponse, SimpleAckPDU): + if _debug: WriteSomethingConsoleCmd._debug(" - not an ack") + return + + sys.stdout.write("ack\n") + + # do something for error/reject/abort + if iocb.ioError: + sys.stdout.write(str(iocb.ioError) + '\n') + + except Exception as error: + WriteSomethingConsoleCmd._exception("exception: %r", error) + + +# +# __main__ +# + +def main(): + global this_application + + # parse the command line arguments + args = ConfigArgumentParser(description=__doc__).parse_args() + + if _debug: _log.debug("initialization") + if _debug: _log.debug(" - args: %r", args) + + # 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), + ) + + # make a simple application + this_application = BIPSimpleApplication(this_device, args.ini.address) + if _debug: _log.debug(" - this_application: %r", this_application) + + # 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 + + # make a console + this_console = WriteSomethingConsoleCmd() + if _debug: _log.debug(" - this_console: %r", this_console) + + # enable sleeping will help with threads + enable_sleeping() + + _log.debug("running") + + run() + + _log.debug("fini") + +if __name__ == "__main__": + main() diff --git a/samples/date_string_patterns.py b/samples/date_string_patterns.py index b5b1b600..04be7755 100644 --- a/samples/date_string_patterns.py +++ b/samples/date_string_patterns.py @@ -22,7 +22,7 @@ def permutation(**kwargs): except Exception as why: test_value = str(why) print(test_string + '\t' + str(test_value)) - print() + print("") for year in year_group: for month in month_group: diff --git a/sandbox/event_detection.py b/sandbox/event_detection.py new file mode 100755 index 00000000..e1957c7a --- /dev/null +++ b/sandbox/event_detection.py @@ -0,0 +1,122 @@ +#!/usr/bin/env python + +""" +""" + +from functools import partial + +from bacpypes.debugging import bacpypes_debugging, ModuleLogger +from bacpypes.consolelogging import ArgumentParser + +from bacpypes.core import run_once + +from bacpypes.service.detect import DetectionAlgorithm, monitor_filter +from bacpypes.object import AnalogValueObject + +# some debugging +_debug = 0 +_log = ModuleLogger(globals()) + +# +# something_changed +# + +def something_changed(thing, old_value, new_value): + print("%r changed from %r to %r" % (thing, old_value, new_value)) + +# +# SampleEventDetection +# + +class SampleEventDetection(DetectionAlgorithm): + + def __init__(self, **kwargs): + if _debug: SampleEventDetection._debug("__init__ %r %r", self, kwargs) + DetectionAlgorithm.__init__(self) + + # provide default values + self.pParameter = None + self.pSetPoint = None + + # bind to the parameter values provided + self.bind(**kwargs) + + @monitor_filter('pParameter') + def parameter_filter(self, old_value, new_value): + if _debug: SampleEventDetection._debug("parameter_filter %r %r", old_value, new_value) + + return (old_value != new_value) + + def execute(self): + if _debug: SampleEventDetection._debug("execute") + + # if _triggered is true this function was called because of some + # parameter change, but could have been called for some other reason + if self._triggered: + if _debug: SampleEventDetection._debug(" - was triggered") + else: + if _debug: SampleEventDetection._debug(" - was not triggered") + + # check for things + if self.pParameter == self.pSetPoint: + if _debug: SampleEventDetection._debug(" - parameter match") + else: + if _debug: SampleEventDetection._debug(" - parameter mismatch") + +bacpypes_debugging(SampleEventDetection) + +# +# +# + +# parse the command line arguments +parser = ArgumentParser(usage=__doc__) +args = parser.parse_args() + +if _debug: _log.debug("initialization") +if _debug: _log.debug(" - args: %r", args) + +# analog value 1 +av1 = AnalogValueObject( + objectIdentifier=('analogValue', 1), + presentValue=75.3, + ) +if _debug: _log.debug(" - av1: %r", av1) + +# add a very simple monitor +av1._property_monitors['presentValue'].append( + partial(something_changed, "av1"), + ) + +# test it +av1.presentValue = 45.6 + +# analog value 2 +av2 = AnalogValueObject( + objectIdentifier=('analogValue', 2), + presentValue=75.3, + ) +if _debug: _log.debug(" - av2: %r", av2) + +# sample event detection +sed = SampleEventDetection( + pParameter=(av1, 'presentValue'), + pSetPoint=(av2, 'presentValue'), + ) +if _debug: _log.debug(" - sed: %r", sed) + +print("") + +av1.presentValue = 12.5 +run_once() + +print("") + +av2.presentValue = 12.5 +run_once() + +print("") + +av1.presentValue = 9.8 +av2.presentValue = 10.3 +run_once() diff --git a/setup.py b/setup.py index 2f593f4b..8526de13 100644 --- a/setup.py +++ b/setup.py @@ -47,6 +47,7 @@ url="https://github.com/JoelBender/bacpypes", packages=[ 'bacpypes', + 'bacpypes.service', ], package_dir={ 'bacpypes': os.path.join(source_folder, 'bacpypes'), diff --git a/tests/test_comm/__init__.py b/tests/test_comm/__init__.py index 51319c95..feaaee4d 100644 --- a/tests/test_comm/__init__.py +++ b/tests/test_comm/__init__.py @@ -11,3 +11,4 @@ from . import test_client from . import test_server +from . import test_capability diff --git a/tests/test_comm/test_capability.py b/tests/test_comm/test_capability.py new file mode 100644 index 00000000..27248a57 --- /dev/null +++ b/tests/test_comm/test_capability.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +""" +Test Capability Module +---------------------- +""" + +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]