Welcome to BACpypes3!
BACpypes3 is a library for building Python applications that communicate using the BACnet protocol. Installation is easy:
$ pip install bacpypes3
You will be installing the latest released version from PyPI. You can also check out the latest version from GitHub:
$ git clone https://github.com/JoelBender/BACpypes3.git
$ cd BACpypes3
And then use the pipenv utility to create a virtual environment, install all of the developer tools, then activate it:
$ pipenv install --dev
$ pipenv shell
Note
If you would like to participate in development, please join the chat room on Gitter.
Getting Started
The bacpypes3 module can be run directly. It will create a small stack of communications objects that are bound together as a BACnet device and present a shell-like prompt:
$ python3 -m bacpypes3
>
The module has a variety options for configuring the small stack for different environments:
$ python3 -m bacpypes3 --help
And the shell has commands that are useful for examining the local BACnet internetwork, its topology, the connected devices, and their objects:
> help
Getting Started
Setup
This tutorial starts with just enough of the basics of BACnet to get a workstation communicating with another device. If you are already familiar with BACnet, skip to the next section.
Basic Assumptions
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 BACnet standard.
If you do not have another device, you can run two BACpypes3 applications or two instances of the same application on one machine but it is slightly more cumbersome to specify source and destination addresses.
Installation
You should have:
a development workstation running some flavor of Linux, Windows, or MacOS, complete with the latest version of Python (>= 3.8) and setup tools and pip. It is also recommended that you use the builtin venv, or pipenv for creating virtual environments.
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.
a BACnetIP/BACnet-MSTP Router if your mysterious device is an MSTP device (BACpypes communicates using BACnet/IPv4 or BACnet/IPv6)
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.
if you are running on Windows it might be beneficial to install the Windows Subsystem for Linux (WSL) and proceed inside that virtual environment.
Before getting this test environment set up and while you are still connected to the internet, create a virtual environment and install the BACpypes library:
$ python3 -m venv myenv
$ cd myenv
$ source bin/activate
$ pip install bacpypes3
$ ...
$ deactivate
or:
$ pipenv --python `which python3`
$ pipenv install bacpypes3
$ pipenv shell
$ ...
$ exit
or to install in the local user path without requiring write access to the system install Python:
$ pip install bacpypes3 --user
or to install in the system (not recommended):
$ sudo pip install bacpypes3
Optional Packages
BACpypes3 has no other dependencies but there are other libraries that will be used if they are available:
websockets is required for BACnet/SC communications (still being developed)
ifaddr is used to resolve interface names with IPv4 and IPv6 addresses. The netifaces package can also be used but it is no longer being maintained.
pyyaml is used to provide YAML configuration files which have some advantages over using JSON or INI formatted files.
rdflib is used for RDF encoding and decoding content for Enterprise Knowledge Graphs and Semantic Web applications.
Installation from Source Code
The GitHub repository contains the source code for the package along with numerous sample applications. Installing the developer version of the package includes the optional packages that are useful for BACpypes3 applications listed above, along with testing using pytest and documentation using sphinx. Install the Git software from here, then make a local copy of the repository by cloning it:
$ git clone https://github.com/JoelBender/BACpypes3.git
$ cd BACpypes3
$ pipenv install --dev
$ pipenv shell
$ ...
$ exit
Wireshark Packet Analysis
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 use BACpypes3! It will prevent you from hours of research when your code won’t work as it should! On a production device you should configure the firewall to only allow traffic to and from specific ports, usually UDP port 47808 and maybe others.
UDP Communications
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 because 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 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.
To receive both unicast and broadcast addresses, BACpypes3 opens two sockets, one for unicast traffic and one that only listens for broadcast messages. The operating system will not allow two applications to open the same socket at the same time so to run two BACnet applications at the same time they need to be configured with different ports.
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.
BACpypes3 Addresses
BACpypes3 addresses are used to communicate between BACnet devices directly (one local station to another) or through a router (a local station through one or more routers to another local station).
There are also special addresses to send a message to all of the stations on the local network (called a “local broadcast”), all of the stations on a specific network through one of more routers (called a “remote broadcast”) or to all of the stations on all of the networks (called a “global broadcast”).
Type |
Example |
Context |
---|---|---|
Local Station |
12 |
ARCNET, MS/TP |
192.168.0.10 |
IPv4, standard port 47808 |
|
192.168.0.11/24 |
IPv4 CIDR, standard port 47808 |
|
192.168.0.12/255.255.255.0 |
IPv4 subnet mask, standard port 47808 |
|
192.168.0.13:47809 |
IPv4, alternate port |
|
192.168.0.14/24:47809 |
IPv4 CIDR, alternate port |
|
192.168.0.15/255.255.255.0:47809 |
IPv4 subnet mask, alternate port 47809 |
|
01:02:03:04:05:06 |
Ethernet address |
|
0x010203 |
VLAN address |
|
[fe80::9873:c319] |
IPv6 |
|
[fe80::9873:c319]:47809 |
IPv6, alternate port 47809 |
|
[fe80::9873:c319/64] |
IPv6 network mask, standard port 47808 |
|
enp0s25 |
Interface name (ifaddr) |
|
Local Broadcast |
* |
|
Remote Station |
100:12 |
ARCNET, MS/TP |
100:192.168.0.16 |
IPv4, standard port 47808 |
|
100:192.168.0.17:47809 |
IPv4, alternate port 47809 |
|
100:0x010203 |
VLAN address |
|
Remote Broadcast |
100:* |
|
Global Broadcast |
*:* |
BACpypes3 has a special addressing mode called route aware that allows applications to bypass the router-to-network discovery and resolution process and send a message to a specific router.
Type |
Example |
Context |
---|---|---|
Remote Station |
200:12@192.168.0.18 |
ARCNET, MS/TP via IPv4 |
200:192.168.0.19@192.168.0.20 |
IPv4 via IPv4 |
|
200:0x030405@192.168.0.20 |
VLAN via IPv4 |
|
Remote Broadcast |
200:*@192.168.0.21 |
ARCNET, MS/TP via IPv4 |
Global Broadcast |
*:*@192.168.0.22 |
all devices via IPv4 |
Object and Property Identifiers
Object Identifiers
Every object in a BACnet device has an object identifier consisting of two parts, an object type and an instance number:
object-type,instance
The object-type is the enumeration name in the ANS.1 definition of the BACnetObjectType in Clause 21 such as binary-input, calendar, or device. The object type may also be an unsigned integer in the range 0..1023.
Note
Enumerated values 0-127 are reserved for definition by ASHRAE. Enumerated values 128-1023 may be used by others subject to the procedures and constraints described in Clause 23.
The instance is an unsigned integer in the range 0..4194303, note that 4194303 is reserved for “uninitialized.”
Caution
BACpypes3 also supports the legacy BACpypes format where the object type and instance are separated by a colon ‘:’ and the object type names are lower-camel-case such as binaryInput. This format is discouraged and may be deprecated in a future version of BACpypes3.
Property Identifiers
Every property of a BACnet object has a property identifier:
property-identifier
The property-identifier is the enumeration name in the ASN.1 definition of the BACnetPropertyIdentifier in Clause 21 such as object-name, description, or present-value. The property identifier may also be an unsigned integer in the range
Note
Enumerated values 0-511 and enumerated values 4194304 and up are reserved for definition by ASHRAE. Enumerated values 512-4194303 may be used by others subject to the procedures and constraints described in Clause 23.
Running BACpypes3
The bacpypes3 module can be run directly. It will create a small stack of communications objects that are bound together as a BACnet device and present a shell-like prompt:
$ python3 -m bacpypes3
>
For these examples assume that the workstation is running on a device with an IPv4 address 192.168.0.10 and its subnet mask is 255.255.255.0. Without extra help, the module will only open the unicast socket and attempts to broadcast traffic will result in a run time error “no broadcast”.
Communications Configuration
The default stack is for a “normal” IPv4 device. In addition to a unicast communication socket, if it is provided the size of the subnet then it will also open a listen-only broadcast socket using the same port number and shared by all of the devices on the subnet.
Normal Device
The bacpypes3 module can be provided with an –address option which not only provides the IPv4 address to use, but also the subnet mask. For example this uses the CIDR notation:
$ python3 -m bacpypes3 --address 192.168.0.10/24
And this uses the subnet mask notation:
$ python3 -m bacpypes3 --address 192.168.0.10/255.255.255.0
Note
The most of the examples in this documentation and sample code will use the CIDR notation.
If there is already a BACnet application running on the workstation and the standard port is being used, the address can also specify an alternate port number:
$ python3 -m bacpypes3 --address 192.168.0.10/24:47809
If ifaddr is installed then the user can provide the interface name:
$ python3 -m bacpypes3 --address enp0s25
The interface names can be listed with the ip link show command in Linux.
Foreign Device
If the workstation is not on the same IP network as other devices, it can register as a foreign device with a time-to-live option:
$ python3 -m bacpypes3 --foreign 192.168.1.11 --ttl 30
It will use an ephemeral port for unicast messages to the other devices and reject/drop IPv4 broadcast messages it receives. It will still receive BACnet broadcast messages that have been forwarded by the BBMD, these are IPv4 unicast messages.
BACnet Broadcast Management Device
The workstation can also be a BBMD when it is provided a Broadcast Distribution Table (BDT):
$ python3 -m bacpypes3 --address 192.168.0.10/24 --bbmd 192.168.0.10 192.168.1.11
Device Configuration
BACnet devices require that they have a device object with some properties that uniquely identify on the network such as a name and a device instance number. These options are also available:
$ python3 -m bacpypes3 --name Intrepid --instance 998
The default name is Excelsior and the default instance number is 999.
The device object also has a vendor-identifier property which is used to understand what proprietary objects and/or properties are available:
$ python3 -m bacpypes3 --vendor-identifier 888
The default vendor identifier is 999, the vendor identifier used for sample applications is 888. Vendor identifiers are free and assigned by ASHRAE.
Shell Commands
Who-Is
Usage:
> whois [ address [ low_limit high_limit ] ]
This command sends a Who-Is Request (Clause 16.10.1) and waits for one or more responses. It will wait up to three seconds for responses, and if the high limit and low limit are identical it will complete as soon as the device responds, if at all.
The address can be any of the five types of addresses; local station, local broadcast, remote station, remote broadcast, or global broadcast. For address syntax patterns see BACpypes3 Addresses.
The low_limit and high_limit are both unsigned integers less than or equal to 4194303. Note that 4194303 is a special device identifier reserved for devices that have not been configured.
Note
Clause 12.1.1
Object properties that contain values whose datatype is BACnetObjectIdentifier may use 4194303 as the instance number to indicate that the property is uninitialized, disabled, or unused, except where noted in individual clauses.
Note
Clause 19.7.1
A Device in a BACnet network might have a network MAC address, but require a Device Identifier, and still be connected to the network. Discovering these unconfigured devices may be performed by using the Who-Is service parameters Device Instance Range Low Limit with a value of 4194303, and Device Instance Range High Limit with a value of 4194303. These unconfigured devices respond with Who-Am-I service. The discovered devices can then be assigned a valid Device Identifier using the You-Are service.
I-Am
Usage:
> iam [ address ]
This command sends an I-Am Request (Clause 16.10.2) with the contents of the appropriate properties of the device object.
The address can be any of the five types of addresses; local station, local broadcast, remote station, remote broadcast, or global broadcast. For address syntax patterns see BACpypes3 Addresses.
Who-Has
Usage:
> whohas [ low_limit high_limit ] [ objid ] [ objname ] [ address ]
This is a long line of text.
I-Have
Usage:
> ihave objid objname [ address ]
This command sends an I-Have Request (Clause 16.9.3)
The objid is an object identifier. For object identifier syntax see Object and Property Identifiers.
The objname is an object name.
The address can be any of the five types of addresses; local station, local broadcast, remote station, remote broadcast, or global broadcast. For address syntax patterns see BACpypes3 Addresses.
Read-Property
Usage:
> read address objid prop[indx]
The address is a local station or remote station. For address syntax patterns see BACpypes3 Addresses.
The objid is the object identifier of the object in the device. For object identifier syntax see Object and Property Identifiers.
The prop[indx] is the property identifier optionally followed by an array index enclosed in square brackets following BACnet rules for arrays. For example, this will read the present-value of the Analog Value Object (Clause 12.4.4):
> read 192.168.0.18 analog-value,2 present-value
This will read the entire priority-array:
> read 192.168.0.19 analog-output,3 priority-array
This will read the length of the object-list:
> read 192.168.0.20 device,1001 object-list[0]
This will read the third element of the object-list:
> read 192.168.0.21 device,1002 object-list[3]
Write-Property
Usage:
> write address objid prop[indx] value [ priority ]
This command sends a Write Property Request (Clause 15.9) and waits for the response.
The address is a local station or remote station. For address syntax patterns see BACpypes3 Addresses.
The objid is the object identifier of the object in the device. For object identifier syntax see Object and Property Identifiers.
The prop[indx] is the property identifier optionally followed by an array index enclosed in square brackets following BACnet rules for arrays.
The value is the value to write to the property. The syntax of the value depends on the datatype of the property being written.
The optional priority is an unsigned integer in the range 1..16.
For example, this will write to the present-value of the Analog Value Object (Clause 12.4.4):
> write 192.168.0.18 analog-value,2 present-value 75.3
This will command the Analog Output present value to 81.2 at priority level 10:
> write 192.168.0.19 analog-output,3 present-value 80.2 10
This will release the command from the previous command:
> write 192.168.0.19 analog-output,3 present-value null 10
Note
Primitive values can be written from the module but the shell commands are simple. Writing arrays and structures (sequences) can be written through code.
Read-Property-Multiple
Usage:
> rpm address ( objid ( prop[indx] )... )...
This command sends a Read Property Multiple Request (Clause 15.7) to the device and decodes the response.
The address is a local station or remote station. For address syntax patterns see BACpypes3 Addresses.
The objid is the object identifier of the object in the device. For object identifier syntax see Object and Property Identifiers.
The prop[indx] is the property identifier optionally followed by an array index enclosed in square brackets following BACnet rules for arrays.
The property name may also be all, required, or optional.
For example, this command will read the values of all of the required properties of the Binary Value Object:
> rpm 192.168.0.20 binary-value,12 all
Who-Is-Router-To-Network
Usage:
> wirtn [ address [ network ] ]
This command sends a Who-Is-Router-To-Network (Clause 6.4.1) to another device requesting it to respond with an I-Am-Router-To-Network (Clause 6.4.2).
The address is typically a local broadcast address used for determining the network topology relative to the requesting device. This command supports sending it to any of the five address types; local station, local broadcast, remote station, remote broadcast, or global broadcast. For address syntax patterns see BACpypes3 Addresses.
The optional network is a unsigned integer in the range 0..65534.
Initialize-Routing-Table
Usage:
> irt [ address ]
This commands sends an Initialize-Routing-Table message to a router with no supplimental routing table information, requesting the router to response with its current routing table.
The address is typically a local station address used for determining the network topology relative to the requesting device. For address syntax patterns see BACpypes3 Addresses.
Read-Broadcast-Distribution-Table
Usage:
> rbdt ip-address
This command sends a Read-Broadcast-Distribution-Table Request (Clause X.Y.Z) to a BBMD which will respond with its broadcast distribution table.
The ip-address is an IPv4 or IPv6 address.
Read-Foreign-Device-Table
Usage:
> rfdt ip-address
This command sends a Read-Foreign-Device-Table Request (Clause X.Y.Z) to a BBMD which will respond with its foreign device table which includes the addresses and the time-to-live of the registered devices.
The ip-address is an IPv4 or IPv6 address.
Configuration
It is convenient to use the BACpypes3 module with the command line parameters to let the module build the appropriate objects and properties, then save that configuration to be used by other applications. The configuration command dumps out the configuration in a variety of formats; JSON, YAML and RDF.
> config json
> config yaml
> config rdf
The output of the BACpypes3 shell can be redirected to a file, so it is quite handy to say this:
$ echo "config json" | python3 -m bacpypes3 > sample-config.json
Debugging BACpypes3
The bacpypes3 module and most BACpypes3 applications use a subclass of the built-in ArgumentParser which includes options for debugging. The library creates loggers for each module and class and the application can attach different log handlers to the loggers.
List of Loggers
The --loggers option is used to list the available loggers:
$ python3 -m bacpypes3 --loggers
This list also contains loggers for other modules and packages.
Debugging a Module
Telling the application to debug a module is simple:
$ python3 -m bacpypes3 --debug
DEBUG:__main__:args: Namespace(loggers=False, debug=[], ...)
DEBUG:__main__:app: <bacpypes3.app.Application object at 0x7f35a4dff880>
DEBUG:__main__:local_adapter: <bacpypes3.netservice.NetworkAdapter object at 0x7f35a4dfe3b0>
<bacpypes3.netservice.NetworkAdapter object at 0x7f35a4dfe3b0>
adapterSAP = <bacpypes3.netservice.NetworkServiceAccessPoint object at 0x7f35a4dfeb00>
adapterAddr = <IPv4Address 10.0.1.90>
adapterNetConfigured = 0
DEBUG:__main__:bvll_sap: <bacpypes3.ipv4.link.NormalLinkLayer object at 0x7f35a4dfe860>
DEBUG:__main__:bvll_ase: <bacpypes3.ipv4.service.BVLLServiceElement object at 0x7f35a4dfe8f0>
>
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 the application has printed out the Namespace instance resulting from parsing the arguments, an Application instance was created, a NetworkAdapter was created which contians some interesting attributes like a reference to a NetworkServiceAccessPoint, and others.
Debugging a Class
Debugging classes and functions can generate a lot of output, so it is useful to focus on a specific function or class:
$ python3 -m bacpypes3 --debug bacpypes3.netservice.NetworkAdapter
DEBUG:bacpypes3.netservice.NetworkAdapter:__init__ ...
<bacpypes3.netservice.NetworkServiceAccessPoint object at 0x7f72ebc52b00>
adapters = {}
router_info_cache = <bacpypes3.netservice.RouterInfoCache object at 0x7f72ebc53fd0>
>
This same method can be used to debug the activity of a an object:
$ python3 -m bacpypes3 --debug bacpypes3.ipv4.IPv4DatagramServer
DEBUG:bacpypes3.ipv4.IPv4DatagramServer:__init__ <IPv4Address 10.0.1.90> no_broadcast=False
DEBUG:bacpypes3.ipv4.IPv4DatagramServer: - local_address: ('10.0.1.90', 47808)
DEBUG:bacpypes3.ipv4.IPv4DatagramServer: - local_endpoint_task: ...
DEBUG:bacpypes3.ipv4.IPv4DatagramServer:set_local_transport_protocol ...
...
> whois
DEBUG:bacpypes3.ipv4.IPv4DatagramServer:indication <bacpypes3.pdu.PDU object at 0x7f04fbbe7490>
<bacpypes3.pdu.PDU object at 0x7f04fbbe7490>
pduDestination = <LocalBroadcast *>
pduExpectingReply = False
pduNetworkPriority = 0
pduData = x'81.0b.00.0c.01.20.ff.ff.00.ff.10.08'
...
In this sample a low level object has had its indication() function called with a message to be sent to all of the devices on the local network (the destination of the UDP message is a localbroadcast) and some more decoding shows that this is an original broadcast BVLL message that is a global broadcast request.
Sending Debug Log to a file
The --debug takes a list of loggers and attaches a StreamHandler which sends the output to stderr be default. With many applications it is useful to redirect that output to a file for later analysis. For example, this will redirect the debugging output of the __main__ module to the test-001.log file:
$ python3 -m bacpypes3 --debug __main__:test-001.log
These log files can become quite large, so you can redirect the debugging to a RotatingFileHandler by providing a file name, and optionally maximum size and backup count. For example, this invocation sends the main application debugging to standard error and the debugging output of the bacpypes.udp module to the test-002.log file:
$ python3 -m bacpypes3 --debug bacpypes3.app.Application:test-002.log: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:
$ python3 -m bacpypes3 --debug bacpypes3.app.Application:test-003.log:1048576:100
Caution
The traffic.txt file will be saved in the local directory (pwd)
Turning on Color
The world is not always black and white, with the output of multiple handlers being displayed it can be difficult to see patterns of activity between loggers, the --color option outputs each logger in a different color.
This tutorial starts with just enough of the basics of BACnet to get a workstation communicating with another device. If you are already familiar with BACnet, skip to the next section.
Samples
The samples can be used as a jumping off point to build your own applications or to incorporate BACnet communications into your existing project.
BACpypes3 applications are built in layers with a stack consisting of some kind of link layer, a common network layer, and a customized application layer. The layers communicate with each other using a client/server design pattern, start with the console.py application to learn about this pattern.
Console Samples
These samples are designed to be run in combination from different terminal/shell windows with different combinations of initialization command line parameters or configuration files.
console.py
This application accepts a line of input from stdin, converts it to uppercase, and echos it back to stdout. But more than that, it is a template for other types of applications.
Client/Server Pattern
This pattern is a way to make module instances work in layers that follow Clause 5.1.1 Confirmed Application Services but rather than horizontal as shown in Figure 5-3 vertical in a stack.

When an instance of a Client wants to make a request, which the BACnet standard calls a CONF_SERV.request primitive, it calls a request() method and passes the Protocol Data Unit (PDU) as a parameter. The Server instance will be notified of this CONF_SERV.indication primitive by having its indication() method called which must be provided by a subclass. See Figure 1.
When the Server instance has content to return to the Client, it calls its response() method and the client will have its confirmation() method called which must be provided by a subclass.
Figure 2 shows an example where a single object in the middle of a stack can be both a client and a server at the same time. The datatypes of a particular client and server pair must match, but the datatypes used by the server side of an object (the top half of the figure) are not the same as the client side (the bottom half of the figure).
The request() to indication() message passing is termed downstream and the response() to confirmation() is termed upstream.
Note
For confirmed services it is expected that a downstream request will be matched with an upstream response, but this is not part of the client/server pattern which is only exchanging PDUs. For example, a Who-Is Request (Clause 16.10.1) is sent downstream by the application layer of a client and is also received upstream by its peers.
Application Code
This description of the application code is in different related chunks rather than being a strict walk though. Starting with basic importing:
import asyncio
from typing import Callable
from bacpypes3.settings import settings
The settings are an object that will start out containing simple BACpypes3 options and will be expanded in other examples. Next is debugging:
from bacpypes3.debugging import bacpypes_debugging, ModuleLogger
...
# some debugging
_debug = 0
_log = ModuleLogger(globals())
These pieces are built on the built-in logging module to provide a way to debug application content. This application uses the ArgumentParser which is extended from the built-in class and includes and processes debugging options:
from bacpypes3.argparse import ArgumentParser
This application uses the Console class for interaction with the user and references a ConsolePDU for “receiving input” (lines of text from the user interactively or from a re-directed input file or pipe) and “sending output” (also lines of text):
from bacpypes3.console import Console, ConsolePDU
The Console is a subclass of Client so it sits at the top of the stack and will send the console input downstream to some Server instance, and accept upstream packets from the server and print or write it.
Rather than sending or receving content through sockets on a network, this sample accepts the content, translates it to uppercase, and sends it back:
from bacpypes3.comm import Server, ...
@bacpypes_debugging
class Echo(Server[ConsolePDU]):
async def indication(self, pdu: ConsolePDU) -> None:
...
if pdu is None:
return
# send the uppercase content back up the stack
await self.response(pdu.upper())
The main function creates instances of these two objects, binds them together, and then waits for the console to say it is finished. Everything else happens via asyncio:
# build a very small stack
console = Console()
echo = Echo()
# bind the two objects together, top down
bind(console, echo)
# run until the console is done, canceled or EOF
await console.fini.wait()
Running the sample
Running the application doesn’t look like a whole lot is going on, Ctrl-D or the platform end-of-file, or Ctrl-C to quit:
$ python3 console.py
> Hi there!
HI THERE!
>
The console input is after the prompt and it accepts input from a file or pipe:
$ echo "Hi there!" | python3 console.py
HI THERE!
Debug a little
Turning on debugging generates some simple log messages for the __main__ logger. The first part is some basic initialization stuff:
$ python3 console.py --debug
DEBUG:__main__:args: Namespace(loggers=False, debug=[], color=None, route_aware=None)
DEBUG:__main__:settings: {'debug': ['__main__'], 'color': False,
'debug_file': '', 'max_bytes': 1048576, 'backup_count': 5,
'route_aware': False, 'cov_lifetime': 60, 'config': {}
}
DEBUG:__main__:console, echo: <bacpypes3.console.Console object at 0x7f3c35b99960>, <__main__.Echo object at 0x7f3c35cfae00>
The next piece has the input after the prompt, then shows that the indication() method of the Echo class instance was called with the content:
> Hi there!
DEBUG:__main__.Echo:indication 'Hi there!'
If the method had any other debugging it would be shown here. After it sends the string back up the stack, it is printed by the Console class instance:
HI THERE!
Last but not least, the end-of-file from the console is sent down the stack as None and the application terminates:
>
DEBUG:__main__.Echo:indication None
Debug a little more
Now turn on debugging for the main application and console class at the same time by listing the name of modules and/or classes:
$ python3 console.py --debug __main__ bacpypes3.console.Console
In addition to the logging lines above, there are some new ones. This is from the initialization of the Console class with the values of its optional arguments:
DEBUG:bacpypes3.console.Console:__init__ '> ' 'console.py.history' None
When the Echo class sends its content upstream, the Console class has its confirmation() method called:
> Hi there!
DEBUG:__main__.Echo:indication 'Hi there!'
DEBUG:bacpypes3.console.Console:confirmation 'HI THERE!'
HI THERE!
And for the end-of-file exception there is some additional logging:
> DEBUG:bacpypes3.console.Console:console_input exception: EOFError
DEBUG:__main__.Echo:indication None
See the section on Debugging BACpypes3 for more debugging options.
console-prompt.py
This is almost identical to the console.py example, all this does is add a --prompt option.
The ArgumentParser class in BACpypes3 is a subclass of the built-in class with the same name, so extending it is simple:
args = ArgumentParser().parse_args()
The code is replaced with this:
# add a way to change the console prompt
parser = ArgumentParser()
parser.add_argument(
"--prompt",
type=str,
help="change the prompt",
default="> ",
)
args = parser.parse_args()
And the Console constructor changes from this:
console = Console()
to include the prompt:
console = Console(prompt=args.prompt)
Turning on debugging shows the new argument along with the others that are built-in:
$ python3 console-prompt.py --prompt '? ' --debug
DEBUG:__main__:args: Namespace(..., prompt='? ')
DEBUG:__main__:settings: {'debug': ['__main__'], 'color': False, ... }
?
console-ini.py
This is a long line of text.
console-json.py
This is a long line of text.
console-yaml.py
This is a long line of text.
Command Samples
The BACpypes3 Cmd class …
VLAN Samples
BACpypes3 has a Virtual Local Area Network (VLAN) concept which is for building networks of devices within an application. For example, a BACnet-to-MODBUS gateway that is designed to present each of the other MODBUS devices as a BACnet device on a virtual network, then the application will be seen as a BACnet router to this virtual network.
It is also very handy in testing, for example different combinations of clients and servers can be collected together to see how they behave all within one application.
It is also useful for tutorials explaining how the BACnet network layer works.
Common Code
BACnet is a peer-to-peer protocol so devices can be clients (issue requests) and servers (provide responses) and are often both at the same time. The start-here.py sample application has just enough code to make itself known on the network and can be used as a starting point for predominantly client-like applications (reading data from other devices) or server-like applications (gateways). The console samples in this group show different options for configuring applications.
start-here.py
This is a good place to start.
BACnet is a peer-to-peer protocol so devices can be clients (issue requests) and servers (provide responses) and are often both at the same time. This sample has just enough code to make itself known on the network and can be used as a starting point for predominantly client-like applications (reading data from other devices) or server-like applications (gateways).
The SimpleArgumentParser is the same one the module uses, see Running BACpypes3:
args = SimpleArgumentParser().parse_args()
if _debug:
_log.debug("args: %r", args)
The Application class has different class methods for building an application stack, the from_args() method is looks for the options from the simple argument parser. Custom applications can add additional options and/or use their own subclass:
# build an application
app = Application.from_args(args)
if _debug:
_log.debug("app: %r", app)
Server-like applications just run:
# like running forever
await asyncio.Future()
Client Samples
If you are building applications that browse around the BACnet network and read or write property values of objects, these are good starting points. Some of these are stand-alone application versions of the shell commands.
read-property.py
This is a long line of text.
write-property.py
This is a long line of text.
read-batch.py
This is a long line of text.
read-bbmd.py
This is a long line of text.
who-has.py
This is a long line of text.
custom-client.py
This is a long line of text.
cov-client.py
This is a long line of text.
Browsing around the network to initialize or synchronize a local database with the BACnet network is so common that there are example applications for that.
Server Samples
Docker Samples
These samples are examples of building and running docker images, these applications and scripts are in a docker subfolder.
Docker Samples
Running BACnet applications in docker is a challenge because the environment that is normally presented to applications is not the one that the host has because the intent is to protect containers (running images) from interfering with or being interfered with by other devices than the host.
Docker has a --network host option so the container does not get its own IP address allocated, which is helpful, but only for unicast traffic. Attempts to “bind” to broadcast addresses of the host fail.
The solution is to have BACpypes3 applications be configured as a foreign device and register with a BBMD on the network. To allow more than one application running on the same host, the application uses an ephemeral port and the operating system will proved provide an unused socket.
A side effect is that subsequent runs of the same image will present most likely not have the same port number, so they will have different BACnet/IPv4 addresses each time they are run. During debugging it might be beneficial to assign a fixed socket number.
This also means that the container can register as a foreign device with some BBMD in the BACnet intranet.
Setting Up
The docker samples are often used with unpublished versions of BACpypes3, so the build scripts include a local build of the package as a wheel.
The root folder of the BACpypes3 project has a bdist.sh script which builds Python eggs and a wheel.
Link Layer Samples
These samples were used during development of the IPv4, IPv6 and SC link layers.
Miscellaneous
These samples are interesting bits and pieces.
apdu-hex-decode.py
This is a long line of text.
console-ase-sap.py
This is a long line of text.
custom-cache.py
This application is a start of a custom device information cache implementation that uses Redis as a shared cache for DeviceInfo records.
The Redis keys will be bacnet:dev:address where address is the address of the device and may include the network like 10:1.2.3.4 for an IPv4 device on network 10.
This application is also a start of a custom routing information cache implementation that also uses Redis as a shared cache for Who-Is-Router-To-Network content. When a request is sent to a remote station or remote broadcast, the cache is used to return the address of the router to the destination network.
The Redis keys will be bacnet:rtn:snet:dnet and the key value is the address of the router on the snet that is the router to the dnet.
Glossary
Glossary
- BACnet
BACnet (Building Automation and Control Network) is the global data communications standard for building automation and control networks. It provides a vendor-independent networking solution to enable interoperability among equipment and control devices for a wide range of building automation applications. BACnet enables interoperability by defining communications messages, formats and rules for exchanging data, commands, and status information. BACnet provides the data communications infrastructure for intelligent buildings and is implemented in hundreds of thousands of buildings around the world.
The BACnet standard was developed and is continuously maintained by the BACnet Committee, more formally known as SSPC 135 (a Standing Standards Project Committee) of the American Society of Heating, Refrigerating and Air-Conditioning Engineers (ASHRAE).
BACnet is an ISO standard (EN ISO 16484-5), a European standard (DIN EN ISO 16484-5:2017-12) and a national standard in many countries.
- BACnet device
Any device, real or virtual, that supports digital communication using the BACnet protocol.
- BACnet network
A network of BACnet devices that share the MAC or VMAC address space under a particular BACnet network number.
- BACnet internetwork
A set of two or more networks interconnected by BACnet routers. In a BACnet internetwork interconnected by BACnet routers, there exists exactly one message path between any two nodes.
- BACnet Device
Any device, real or virtual, that supports digital communication using the BACnet protocol.
- ephemeral port
An ephemeral port is a communications endpoint (port) of a transport layer protocol of the Internet protocol suite that is used for only a short period of time for the duration of a communication session. Such short-lived ports are allocated automatically within a predefined range of port numbers by the IP stack software of a computer operating system. Wikipedia
- upstream
Something going up a stack from a server to client.
- downstream
Something going down a stack from a client to a server.
- stack
A sequence of communication objects organized in a semi-linear sequence from the application layer at the top to the physical networking layer(s) at the bottom.
- discoverable
Something that can be determined using a combination of BACnet objects, properties and services. For example, discovering the network topology by using Who-Is-Router-To-Network, or knowing what objects are defined in a device by reading the object-list property.