Build a BACnet datalogger with a Raspberry

A new open source plugin allows BACnet MS/TP communication directly from within WarpScript. Custom datalogger has never been easier…

Build a BACnet datalogger with a Raspberry

This week, I released a new open source plugin for Warp 10. It allows reading and writing registries over BACnet protocol. This protocol is heavily used in climate control units (rarely the ones for homes, but for buildings). With only six new WarpScript functions, you can build a BACnet datalogger with the hardware you want.

king-size IoT
This is king-size IoT.

BACnet introduction

This protocol was invented before IP. It allows devices to discover themselves (whois broadcast), to announce their capabilities (hello, I am a sensor), to subscribe to each other to data… 30 years of features stacked upon each other. This should raise a red flag in your mind: «Do not even try to write a driver from scratch».

BACNet layers
Don't try to rewrite that. (Source)

In the Java world, there is BACnet 4J. It is nearly undocumented and bacnet4j implementation is awful. But it is a GPL license, and that is good. So, my Warp 10 plugin is also GPL. Feel free to fork and contribute!

What you need

Hardware

Raspberry

Any kind of Linux hardware with a USB port: A simple headless Raspberry will do the job.

You need a RS485 interface for BACnet MS/TP:

RS 485
RS 485 hardware

This one (amazon link) is well designed (FTDI chip, tx and rx LEDs, 1kV isolation, no risk of ground loop).

Once started, BACnet communication never ends. Master and slave keep communicating, the bus never sleeps. Don't be surprised that LEDs never stop blinking.

Connect the (optional) cable shield to GND of your device, or GND of the interface (not both).

Documentation

There are 2 ways to proceed:

  • You sign a NDA with the supplier, and then you will have the BACnet mapping of the devices you want to play with.
  • You explore each BACnet registry, for all registry types, and you hope the "objectName", "description" "objectType" "units" properties are available… then you can make smart guesses.

The first solution is way simpler.

Deploy Warp 10 and BACnet plugin

To deploy Warp 10 on your datalogger device, follow the Warp 10 Getting Started.

To deploy the BACnet plugin, clone the repo locally (on your computer, if jdk8 is installed), and compile the jar:

git clone "https://github.com/senx/warp10-plugin-bacnet.git"
cd warp10-plugin-bacnet
./gradlew shadowJar
cp build/libs/warp10-plugin-bacnet.jar /opt/warp10/lib/
cp io.warp10.plugins-bacnet-plugin.conf /opt/warp10/etc/conf.d/
sudo systemctl restart warp10.service

The warp10 user must be in the dialout group to be able to open a serial port.

usermod -a -G dialout warp10

Test the connection

Once plugged in, your RS485 adapter should be recognized as /dev/ttyUSB0, if there is no other ftdi chip connected to the system.

I assume you know the address of the device. In the following examples, I will communicate with 620001, at 19200 baud, without any parity.

Your first WarpScript test will open the port, and get an object that represents the remote 620001 device.

// @endpoint http://myRPi:8080/api/v0/exec

BACnetIsOpened ! // check if port is not already opened...
<% 
  {
    'port' '/dev/ttyUSB0'
    'baudrate' 19200
    'databits' 8
    'stopbits' 1
    'parity' 'none'
  } BACnetOpenLocalDevice   // do not returns anything. 
%> IFT

What does the plugin do?

  • First, it checks if '/dev/ttyUSB0' is really a serial port.
  • Then it opens the port, creates a network, then a local device (address 0, objectName "Warp10Node", modelName "BACnet4J")
  • These operations should only take a few milliseconds.

Once opened, you should see tx and rx LED frantically blinking, even if you did not try to communicate. It means the BACnet is up, your slave device is ready to exchange packets. If you don't see RX led blinking, you have a communication problem. Check connections and baud rate.

The next step is to open a communication channel with your device:


// open device #620001  with a 15000 millisecond timeout
620001 15000 BACnetOpenRemoteDevice  'remoteDevice' STORE

What does the plugin do?

  • It is the first real communication attempt with the device. It can take a few seconds, or fail with a timeout.
  • Once done successfully, the node coordinates are kept in cache. The next call, this function should be immediate.

Store the result in a WarpScript variable, it will be necessary for the next steps.

Poll BACnet variables

Once you get an object for your remote device, you can try to read and write values.

To do so, you will request objects identifiers to the remote device. To build objects identifier, you must know their address and the type. Supported types in the plugin are:

  • analogInput
  • analogOutput
  • analogValue
  • binaryInput
  • binaryOutput
  • binaryValue
  • multiStateInput
  • multiStateOutput
  • multiStateValue

Each object has several properties. The most interesting is of course "present value", but the function BACnetReadObjects have an optional boolean to also read "objectName", "description", "objectType" and "units". The more properties you ask for, the slower is reading operation is, but it is useful for debugging.

For the sake of simplicity, the BACnetReadObjects take a map where you can name objects, and it returns a map with the same keys. It is far easier to manipulate the result!

true 'showAllBacnetProperties' STORE

$remoteDevice
{
  "CurrentTemperatureZ1"   "analogValue" 101 BACnetBuildObjectId
  "CurrentSetPointZ1"      "analogValue" 102 BACnetBuildObjectId
  "CurrentTemperatureZ2"   "analogValue" 201 BACnetBuildObjectId
  "CurrentSetPointZ2"      "analogValue" 202 BACnetBuildObjectId
} $showAllBacnetProperties  BACnetReadObjects

…Will return a MAP:

  {
    "CurrentTemperatureZ1": {
      "present-value": "19.0",
      "units": "degrees-celsius",
      "object-name": "Zone1TPie",
      "description": "Zone1TPie",
      "object-type": "analog-value"
    },
    "CurrentSetPointZ1": {
      "present-value": "20.5",
      "units": "degrees-celsius",
      "object-name": "Zone1TPiePC",
      "description": "Zone1TPiePC",
      "object-type": "analog-value"
    },
    "CurrentTemperatureZ2": {
      "present-value": "19.08",
      "units": "degrees-celsius",
      "object-name": "Zone2TPie",
      "description": "Zone2TPie",
      "object-type": "analog-value"
    },
    "CurrentSetPointZ2": {
      "present-value": "18.0",
      "units": "degrees-celsius",
      "object-name": "Zone2TPiePC",
      "description": "Zone2TPiePC",
      "object-type": "analog-value"
    }
  }

If the property does not exist, or if the object type is wrong, the function does not fail, but returns errors:

{
    "V101": {
      "present-value": "errorClass=object, errorCode=unknown-object",
      "units": "errorClass=object, errorCode=unknown-object",
      "object-name": "errorClass=object, errorCode=unknown-object",
      "description": "errorClass=object, errorCode=unknown-object",
      "object-type": "errorClass=object, errorCode=unknown-object"
    }
}

If you omit the boolean from BACnetReadObjects parameters, the result has just the present value:

{
    "CurrentTemperatureZ1": {
      "present-value": "19.0"
    },
    "CurrentSetPointZ1": {
      "present-value": "20.5"
    },
    "CurrentTemperatureZ2": {
      "present-value": "19.08"
    },
    "CurrentSetPointZ2": {
      "present-value": "18.0"
    }
}

From this MAP, the conversion of results to GTS is straightforward. The complete example of a BACnet datalogger is here.

  • BACnetIsOpened: check whether BACnet is up.
  • BACnetOpenLocalDevice: setup BACnet network
  • BACnetOpenRemoteDevice: find a device on the network
  • BACnetBuildObjectId: create an object
  • BACnetReadObjects: read objects

Writing on BACnet

Why would you do that? Because Warp 10 can be used to build a full IoT platform. It means you can set a downstream channel, for example, to change a temperature setpoint or reset failures remotely.

So, the last plugin function is BACnetWriteObjects: It takes the same objects as BACnetReadObjects, and writes "present value" property.

$remoteDevice
  "analogValue" 60 BACnetBuildObjectId 
  1.0  
BACnetWriteObjects  // set AV60 to 1.0
 
$remoteDevice 
  "multiStateValue" 2 BACnetBuildObjectId  
  2 
BACnetWriteObjects  // set MV2 to 2

$remoteDevice
  "binaryValue" 1107 BACnetBuildObjectId 
  false
BACnetWriteObjects  // set BV 1107 to inactive

Set up a runner

Warp 10 has a built-in scheduler: Just copy your WarpScript files to the right place… For example, /opt/warp10/warpscripts/myproject/5000/bacnet-polling.mc2 will be executed every five seconds (5000ms). To build a BACnet datalogger, you just need to move your WarpScript to poll values and store them regularly…

If you need to keep variables from one execution to another, you can let them in RAM memory with SHM extension.

Miscellaneous

  • The plugin checks every 10 seconds if the serial port still exists. If not, it closes every connection.
  • For the moment, there is no support for multiple connections. Calling BACnetOpenLocalDevice closes all opened connections before opening a new one.
  • There is no BACnet over IP support, but it is easy to add. Feel free to contribute.

Conclusion

This BACnet datalogger example and source code will definitely help anyone who tries to use BACnet 4J. It shows you how to build a simple plugin for Warp 10: Add new functions to WarpLib (like an extension), and run a thread in the background to monitor the network.