Your CuPID as a PLC: Example Modbus Client with LabJack multifunction DAQ

Step 1: Choose a Modbus Server

Servers abound: other PLCs, touchscreens, temperature controllers, VFDs, DAQs, other Pis; nearly anything on the network these days has modbus functionality built in. It is, for better or worse, the least common denominator in device communication. We wrote a bit about this topic previously.

Here, we’ll demonstrate communication with a LabJack DAQ. They offer very reasonably priced hardware with some great functionality. We’ve used them for many years, built LabVIEW applications for them, and put them to test in industrial data acquisition environments. Bang for buck, you just can’t beat them.

Incredibly, outside of the well-written LJUD driver (which our LabVIEW applications are built on), each of their devices also acts as a modbus server for all available data. All you need is a map of registers and which values they correspond to, which they provide here.

Map out the values

With the modbus register map, all we need to do is decide what we want to read, choose the appropriate registers, read them, and make sure we interpret them properly into meaningful values.

We’ve got a couple LabJacks laying around. For this example, we’ll use our trusty UE9, which has plenty of IO for what we’re doing here. It’s on our LAN here at the shop, and we’ve set it up to take a static IP.

First, we’ll make sure that we’re talking properly to view the Analog Input (AIN) data. We’ll use AIN0-3 (why and where we’re probing is discussed below). Each value comes in as a float, which occupies four bytes. Each modbus register is a two-byte word, so each AIN will require two modbus registers. This means we can read two registers each from registers 1, 3, 5, and 7 for the four analog inputs. Note that the modbus maps provided by LabJack start at 0, but all modbus data layers start at 1. So all values are offset by one.

We pull up our basic modbus TCP viewer page to view devices. As we discussed previously, we use some ajax and wsgi to pass data back and forth between our browser and our server. In this case, the user initiates a request in the webpage by pressing a ‘data refresh’ button, which passes the register, clientIP, and length of the request to the server via ajax. A wsgi script on the server, in the case ‘wsgiactions.wsgi’, is run with the request information. It calls pymodbus to read our data from our LabJack, and happily returns it to our browser, as shown below:

Our basic MBTCP Viewer allows us to verify we're reading what we want, and the device is responding as we like
Our basic MBTCP Viewer allows us to verify we’re reading what we want, and the device is responding as we like

You can watch all of this magic happen using Firebug, which is how we diagnose all of our web behavior. See at the bottom how we log the data object as it comes from our wsgi script, and can examine the object structure and the DOM as we like. You can even execute js/jQuery on the fly. It’s seriously like magic.

Firebug: how we diagnose our client behavior and ajax queries (css too!). It's pretty amazing stuff.
Firebug: how we diagnose our client behavior and ajax queries (css too!). It’s pretty amazing stuff.

So we’re getting values, and they appear to be legitimate. Next, we need to make a decision about how to format them. These are four-byte float values, which need to be converted into something that resembles, you know, a number. We can do this in the web page itself,  or we can process them as we put them into a database that stores current values and datalogs. We’ll show both, just for fun. Inside the browser, we’ll add an option to select formatting. It will force us to build some formatting and byte-switching functions that we should probably have around anyway.

Float, anyone?


First, we borrow this beautifully simple piece of code from here. It takes an array of bytes and converts it into a float, given a few other parameters that determine the type of conversion. It’s simple enough to plug and play, but flexible enough to allow conversion of 32 and 64 bit floats and varying endianness. First, we simply take our two 16-bit words and break them into bytes:

var byte2 = somedata.response.values[i] % 256
var byte1 = (somedata.response.values[i] - byte2)/256
var byte4 = somedata.response.values[i+1] % 256
var byte3 = (somedata.response.values[i+1] - byte4)/256
var bytes = [byte1, byte2, byte3, byte4]

Then we feed our bytes into our decoder, and out pop values:

var myfloat = decodeFloat(bytes ,1, 8, 23, -126, 127, false)

We’d also like to truncate the displayed values a little bit to make them easier to read. At best, the UE9 will give us 16-bit resolution over the +/-5V range, which comes out to 0.15mV. This means it’s totally safe to give ourselves 5 digits of precision without losing any information. And, while we’re at it, let’s add a widget to the panel to let us set this on the fly, should we want to fix it. We’ll also add a drop-down to allow selection of our float formatting. We add it in, an d presto: human-readable values! We have connected AIN0 to Vs and AIN1 to GND on the LabJack, so we know where our numbers should be. Everything looks pretty good.

Our basic modbus tcp/ip viewer, with float conversion and adjustable precision. We’ve got Vs on AIN0 and GND on AIN1, so everything looks proper.

In python, our life is easier. Everything we need is built into standard packages, namely the struct package. First, we break the words into bytes as we did previously:

byte2 = somedata.response.values[i] % 256
byte1 = (somedata.response.values[i] - byte2)/256
byte4 = somedata.response.values[i+1] % 256
byte3 = (somedata.response.values[i+1] - byte4)/256

Next, we convert to characters that are string hexadecimal representations of the bytes:

byte1hex = hex(byte1)
byte2hex = hex(byte2)
byte3hex = hex(byte3)
byte4hex = hex(byte4)
hexstring = byte1hex + byte2hex + byte3hex + byte4hex

Finally, we use the struct package to convert a string representation, specifying that we are big-endian (>) and float (f):

import struct
myfloat = struct.unpack('>f',hexstring)

We’ll use this later when we build a general purpose importer for our database values. Easy peasy!

Read and log, please

Now we don’t want to have to keep pressing our data refresh button, so we’re going to set up our Modbus values as inputs for our CuPID. Besides our mouse finger getting tired, we want to datalog, and potentially use these values to trigger external actions, such as outputs, emails, etc.

We define all of the poll and read configuration through database tables, in a database aptly named ‘controldata.db’. A python daemon handles various system maintenance, data hygiene functions, web session management, and most important, our input/output polling and control algorithms. How this works is worth an article in itself, and it’s on the way. For now, suffice it to say that when we create an interface and define IO as below, our CuPID will periodically read and log values to our IO and log databases.

Each time the daemon cycles through and reads IO, it does so by interface. Each type of interface is handled depending on type. For example, 1Wire interfaces have their own routine, as do SPI, I2C, and so on. For the modbus interface type, if the interface is enabled, the ‘modbustcp’ table is searched for those bound to its interface id and processes them as dictated by the table entry.

Defining an interface

Our interfaces are defined in the ‘interfaces’ table, shown here in phpliteadmin:

Our interfaces table as viewed in phpliteadmin.
Our interfaces table as viewed in phpliteadmin.

and here in our interfaces screen:

Interfaces screen as viewed in the CuPID web interface.
Interfaces screen as viewed in the CuPID web interface.

You can see that all the defining parameters are present for our various interfaces. In the case of Modbus TCP/IP, we define the IP here and then individual registers in a separate table, aptly named modbustcp.

The modbustcp table

Each modbustcp table entry  specifies the unique interface ID of the modbus interface (created above), and are hence bound to this interface. In this way, if we change the client ID or basic read parameters, we can do it once in the interface table, rather than in a dozen places. It also enables us to take devices offline in one fell swoop. It has loads of benefits.

In the ‘modbustcp’ table, we specify the read memory locations and size, how to interpret the data, and a number of other options. In light of our work with float conversion above, we add a ‘format’ field, to which we can add our float specification. We add a piece to our IO read handler (see processMBinterface section here), and we’re set. We also add a catch-all ‘options’ field to which we can add any other number of flags and options, in object notation. So, for example, if we want to scale our values and set precision before the values are entered into the database, we can specify:

scale:1, precision:5

Our python control scripts can use a function to parse this into a dictionary and do with it as we see fit. More on this later. Now we just need to add and name our modbustcp entries for our analog input entries. We can do this from our ioedit page:

Adding modbus TCP IO entries for each of our analog inputs.

A little clicky-clicky, and we have four inputs named for the four analog inputs. To get them to show up in our list of inputs, we hit the refresh button. Remember, what shows up in the lists of inputs and outputs are what are actually generated when IO are read by the daemon. Notice that they have values in the right-hand column. Our first analog input (shown below) has a value shown in the bubble. To give them handy names, we edit them in place:

Renaming IO in place gives us handles that help us remember what is attached to what.
Renaming IO in place gives us handles that help us remember what is attached to what.

Now, when we make new  connections, we rename them and they’ll pop up with the names we’ve given them. It’s otherwise quite inconvenient to have to remember what “AIN0” or “MBTCP1_400001_2R” mean. The colloquial names we make are stored in a special table called “ioinfo”, and key an ID to a text name. The IDs are automatically created in such a way as to make them unique, and the labels are stored even after the IO is removed. If you add it back in again, the label will still be there.

See the Data

Now, we want to see the data. First, we verify that it’s being recorded as we wish. We won’t get into the details (we’ll do that elsewhere), but default parameters dictate that each input that is read is logged with the same regularity it’s read. We can specify how much is retained. The default here is 100 points. Either using phpliteadmin or our ‘dataviewer’ page, we can see they indeed exist, and we select them to be viewed using the toggles:

Logs show up with the number of points indicated by the bubbles. We select the logs we wish to view.
Logs show up with the number of points indicated by the bubbles. We select the logs we wish to view.

Now, we pop over to our datalogviewer to see them in plot form. Using the plot controls at left, we select our newly-created short names and refresh the plot. Like magic, they pop up with our names:

Selected datalogs, viewed in our plot window.

Next, on to a practical example!


The above makes use of the open source libraries available on github here:

Explanation and installation here:

One thought on “Your CuPID as a PLC: Example Modbus Client with LabJack multifunction DAQ”

Leave a Reply

Your email address will not be published. Required fields are marked *