Adventures in Moteino: Remote Temperature Monitor

Introduction

<<Edit>>: See here for next post on enhanced model: http://www.cupidcontrols.com/2014/10/cupid-remote-enhanced-flexible-io-mote/

What every new system design has in common these days is wireless. Like bacon, it just makes everything better. Put a sensor wherever, read it from somewhere else. Put the power and control where you need it. For the CuPID/Pi, it is no different. We want to put our remote sense and control modules out into the wild and read and aggregate them as it makes sense.

Like bacon, wireless makes everything better.
Like bacon, wireless makes everything better.

Our basic system layout is as below. We’ve got multiple wireless nodes that broadcast data periodically, and a controller/aggregator that will log this data, acknowledge receipt, and do something useful with it. Eventually, we may have intermediate powered nodes that serve to mesh the grid out, but for now, our nodes just send data to the controller.

We’re currently using these awesome little RF units, called Moteinos. They are an Arduino clone that can use the standard IDE with their bootloader. They’ve got the ever-so-popular ATMega328P chip that is familiar to anybody working with an Arduino Nano or Uno. The Moteino comes with an RF board using the SPI bus of the microcontroller, available in various frequency flavors, and also in two different power varieties: one for maximum range, and one for maximum battery life. The whole thing comes nicely arranged on a PCB with a built-in voltage regulator, headers to connect our FTDI cable for loading and diagnostics, and an antenna. All for about $20. Really a fantastic deal .. and great customer support. Please patronize them.

Our basic remote unit layout and communication structure. Low power on the remote sensor end, and high functionality on the controller end.
Our basic remote unit layout and communication structure. Low power on the remote sensor end, and high functionality on the controller end.

Anyhow, so we jammed one of these into a box in another post, and now we’d like to complete the job of creating a general purpose remote wireless node, along with getting it talking and doing something useful.

As a post-note, we took the basics we hashed out here and put them into a modular Mote sketch. This post is still useful for a demonstration of how we put these pieces together.

We also went on to build up the backend data processing, and talk about that elsewhere as well. Check it out.

The Project

So let’s get started. Our goals for this project are the following:

  • Get our remote unit reading a 1Wire temperature sensor
  • Get the remote unit periodically broadcasting data in a standard message format
  • Configure the unit to request acknowledgement
  • Read data from the unit from a remote CuPID Control unit
  • Configure the unit to go into a low-power state between read operations after receiving acknowledgement or after fixed number of retries
  • Package the unit into a waterproof enclosure

The Prototype Setup

We set up our remote and gateway units, both connected by USB on virtual COM ports. At right, the remote unit is connected to a 1Wire DS18B20 temperature sensor, with a pull-up resistor on the data line.

Prototype setup for node/gateway test communication. Connection to two virtual COM ports allows reading activity on both Moteinos.
Prototype setup for node/gateway test communication. Connection to two virtual COM ports allows reading activity on both Moteinos.

The Remote Unit

Basic Communication

We’ve got our wireless unit (we’ll use the lower power RFM69 to start, for our remote unit), an FTDI/USB adapter, and a cable to plug her into the computer to do some programming. Let’s get talking. We grab the library from the git repo and put it somewhere our IDE can find it. We grab the SPIFlash repo as well so we can just jump into some example code.  We take the boilerplate ‘sender’ sketch and put it on our low power unit. Nothing surprising there – compiles and uploads fine!  We open up a PuTTY console on our windows box and are happily greeted:

Listening at 433 Mhz...
 SPI Flash Init OK!
Reading Temperature

Now let’s get a temperature sensor wired up and reading. We use a trusty DS18B20, the workhorse of our temperature-sensing toolbox. We hook up ground, power to our 3.3V rail, and data to one of our ATMega digital pins with a 4.7k pullup between it and 3.3V. We’ve got it connected to A0, pin 14 on the ATMega. We modify our sketch to spit out data in ASCII json on the serial port, using a main block like this, using OneWire library code. Not shown is the include line for the OneWire.h library, or a couple subroutines. You can find the complete sketches in the reference section at the end.

// Message Type 
Serial.print("msgtype:");
Serial.print(msgtypestr);
Serial.println(",");
    
// Message Source ID 
Serial.print("sourceid:");
Serial.print(SOURCEID);
Serial.println(",");
    
// Message Dest ID 
Serial.print("destid:");
Serial.print(DESTID);
Serial.println(",");

// Device identifier
byte dsaddr[8];
getfirstdsadd(dsaddr);
    
Serial.print("dsaddress:");
int i;
for (i=0;i<8;i++) {
  Serial.print(dsaddr[i], HEX);
}
Serial.println(',');

// Data
Serial.print("temperature:");
float temp = getdstemp(dsaddr);
Serial.print(temp);

Here are the subroutines for the OneWire stuff:

void getfirstdsadd(byte firstadd[]){
byte i;
byte present = 0;
byte addr[8];
float celsius, fahrenheit;
int length = 8;
//Serial.print("Looking for 1-Wire devices...\n\r");
while(ds.search(addr)) {
//Serial.print("\n\rFound \'1-Wire\' device with address:\n\r");
for( i = 0; i < 8; i++) {
firstadd[i]=addr[i];
//Serial.print("0x");
if (addr[i] < 16) {
//Serial.print('0');
}
//Serial.print(addr[i], HEX);
if (i < 7) {
//Serial.print(", ");
}
}
if ( OneWire::crc8( addr, 7) != addr[7]) {
//Serial.print("CRC is not valid!\n");
return;
}
// the first ROM byte indicates which chip
//Serial.print("\n\raddress:");
//Serial.print(addr[0]);
return;
}
}
float getdstemp(byte addr[8]) {
byte present = 0;
int i;
byte data[12];
byte type_s;
float celsius;
float fahrenheit;
switch (addr[0]) {
case 0x10:
//Serial.println(" Chip = DS18S20"); // or old DS1820
type_s = 1;
break;
case 0x28:
//Serial.println(" Chip = DS18B20");
type_s = 0;
break;
case 0x22:
//Serial.println(" Chip = DS1822");
type_s = 0;
break;
default:
Serial.println("Device is not a DS18x20 family device.");
}
ds.reset();
ds.select(addr);
ds.write(0x44,1); // start conversion, with parasite power on at the end
delay(1000); // maybe 750ms is enough, maybe not
// we might do a ds.depower() here, but the reset will take care of it.
present = ds.reset();
ds.select(addr);
ds.write(0xBE); // Read Scratchpad
//Serial.print(" Data = ");
//Serial.print(present,HEX);
//Serial.print(" ");
for ( i = 0; i < 9; i++) { // we need 9 bytes
data[i] = ds.read();
//Serial.print(data[i], HEX);
//Serial.print(" ");
}
//Serial.print(" CRC=");
//Serial.print(OneWire::crc8(data, 8), HEX);
//Serial.println();
// convert the data to actual temperature
unsigned int raw = (data[1] << 8) | data[0];
if (type_s) {
raw = raw << 3; // 9 bit resolution default
if (data[7] == 0x10) {
// count remain gives full 12 bit resolution
raw = (raw & 0xFFF0) + 12 - data[6];
} else {
byte cfg = (data[4] & 0x60);
if (cfg == 0x00) raw = raw << 3; // 9 bit resolution, 93.75 ms
else if (cfg == 0x20) raw = raw << 2; // 10 bit res, 187.5 ms
else if (cfg == 0x40) raw = raw << 1; // 11 bit res, 375 ms
// default is 12 bit resolution, 750 ms conversion time
}
}
celsius = (float)raw / 16.0;
fahrenheit = celsius * 1.8 + 32.0;
//Serial.print('Celsius:');
//Serial.println(celsius);
return celsius;
}

With this code, our output looks something like this (using PuTTY on our virtual USB COM port):

msgtype:1,
sourceid:87,
destid:1,
dsaddress:2811FDE230092,
temperature:22.00

Perfect.

Going On-Air with RF

Now we just need to format this into a radio-friendly format. This is not quite as easy as it seems. With the serial output, we can just continually send ASCII bytes with no regard to message beginning or end. With radio, this is not the case. We need to formulate our entire message before we send it. We broadcast it with a destination and source ID on a Network ID, put it all together with a CRC to ensure data integrity, and then encrypt the whole lot with shared keys. We are SO grateful that Felix over at LowPowerLab has already done this for us inside of the RFM69 library, as we were not looking forward to writing it all ourselves. It also has Acknowledge implementation, so we just saved days and days of coding. Thank you!

Alright, so on to putting our message into a string of bytes. Reading here, we see that the message can be a maximum of 61 bytes (to support AES encryption). The most efficient way to do encode our message is to definitively split our message up into identifiers and values. We can use either fixed-length fields or delimiters. We’ll use json-style delimiters, as we did in our serial output above. Our identifiers will tell whomever is listening on the other end how to interpret the data. So, for example, we’ll start with an identifier like “tempasc” that will indicate we are sending a temperature in ascii-encoded numeric characters. This is of course not the most efficient way to send the data. For example, we would send a value like 20.0625, taking seven bytes, where if we encoded it as a four-byte float value, we’d both save three bytes and allow for a very wide range of values indeed. We’ll get to that later. Baby steps.

So we initialize a character string of maximum length of 61 bytes and start filling it with our data, and then send it on over:

char sendstring[61];
int sendlength = 16;
int wholePart = temp;
int fractPart = (temp - wholePart) * 10000;
sprintf(sendstring, "tempasc: %d.%04d,", wholePart, fractPart);
    
Serial.println("SENDING");
Serial.println(sendstring);
radio.sendWithRetry(GATEWAYID, sendstring, sendlength);
Serial.println("SEND COMPLETE");

We check out our serial output and see what we see. Everything looks in order:

msgtype:1,
sourceid:87,
destid:1,
dsaddress:2811FDE2300,
temperature:22.25
SENDING
tempasc: 22.2500,
SEND COMPLETE

Since we’re here, we’ll go ahead and add a little metadata:

char sendstring[61];
int sendlength = 61;  // default
int wholePart = temp;
long fractPart = (temp - wholePart) * 10000;
sprintf(sendstring, "tempasc:%3d.%04d,tempdev:ds18x,temprom:%0xx%02x%02x%02x%02x%02x%02x%02x%02x", wholePart, fractPart, dsaddr[0],dsaddr[1],dsaddr[2],dsaddr[3],dsaddr[4],dsaddr[5],dsaddr[6],dsaddr[7]);
sendlength = 57; 
Serial.println("SENDING");
Serial.println(sendstring);
radio.sendWithRetry(GATEWAYID, sendstring, sendlength);
Serial.println("SEND COMPLETE");

This gives us the following on serial output:

SENDING
tempasc: 23.0625,tempdev:ds18x,temprom:0x2811fde203000092
SEND COMPLETE

This will work nicely. As we mentioned previously, we cleaned and wrapped the above code into a modular sketch that we now use for everything.

Sleep, Moteino, Sleep

One of the beautiful things about this setup is that the Moteino is tiny, the sensor is low-power, and the controller is also set up to draw very low power when sleeping. The LowPower library does just this, and quite easily. Also built into the RFM69 library is a simple radio.sleep() function that will put our RF to rest. We implement it at the end of our loop as here:

    Serial.flush();
    radio.sleep();

    for (i=0;i<numloops;i++) {
      LowPower.powerDown(sleepperiod, ADC_OFF, BOD_OFF); 
    // Sleeptime eludes millis()
    }
    looptime += LOOPPERIOD;

The serial flush function fixes some serial garble issues with power down (see discussion here). Also note the last bit where we add in the LOOPPERIOD. As it turns out, millis() stops when we power down the Mote. As a result, we need to manually add the time in to the looptime. We use this in the IO and channels processing, so we want to keep (relatively) accurate time. After these adds, we get the following for a sleep time of 3000:

Going to sleep for 
3000
numloops 12

Looks good! Again, see this post for a more exhaustive description of how the nuts and bolts of these routines work.

Now let’s get to receiving!

The Gateway

Basic Communication

Now let’s make sure we’re receiving what we want on the other end (at the gateway).  Rather than have much intelligence inside of our gateway, we’re going to configure it as a repeater of data over the serial port, which will be attached to our controller, in this case a Raspberry Pi/CuPID Controller. On the Pi, we’ll open a serial connection over TTYAMA0 (the built-in UART) and listen for data from our receiver. We’ll also set up basic commands to have it send commands to remote devices and set internal parameters, but we’ll cover that later. We set up a basic gateway sketch, as provided by our benefactor Felix (LowPowerLab). We make sure our network ID and encryption key match, throw in a RECEIVE begin and end notification, and begin listening. Piece of cake:

BEGIN RECEIVED
[2] tempasc: 23.0625,tempdev:ds18x,temprom:0x2811fde203000092 [RX_RSSI:-10]
END RECEIVED

Now let’s connect our receiver to our Pi and make sure we can get a listener up and reading. We take a high power unit and flash our gateway program to is, with a few mods to make it spit out everything in json-like format, and stick it down to the top of the board in our CuPID controller. We power from the aux pins on the top of the board, and connect our serial RX/TX to the exposed pins on the top of the header:

Putting our gateway RF unit in our CuPID Controller. TX/RX and power, and we’re done.

Now let’s fire up a shell and a serial monitor to make sure everything is coming through right. We use some pretty straightforward code in serialhandler.py to handle our serial input:

def monitor(port='/dev/ttyAMA0',baudrate=115200, timeout=1):
    import serial
    ser = serial.Serial(port=port, baudrate=baudrate, timeout=timeout)
    
    print "Monitoring serial port " + ser.name
    data = []
    while True:
        ch = ser.read(1)
        if len(ch) == 0:
            # rec'd nothing print all
            if len(data) > 0:
                s = ''
                for x in data:
                    s += '%s' % x # ord(x)
                
                # Here for diagnostics
                print '%s [len = %d]' % (s, len(data))

                # now process data
                processserialdata(s)

            data = []
        else:
            data.append(ch)

We process the serial input using a simple parser that we use elsewhere. It takes our data, parses using the beginning and ending messages that we’ve put into our arduino sketch, and converts whats in the middle into a python dictionary.

def processserialdata(data):
    try:
        message = data.strip().split('BEGIN RECEIVED')[1].split('END RECEIVED')[0]
    except:
        print('there was an error processing the message')
        return 
    else:
        from cupid.pilib import parseoptions
        datadict = parseoptions(message)
        return datadict

Present in pilib.py (imported here as cupid.pilib) is our parser:

def parseoptions(optionstring):
    list = optionstring.split(',')
    optionsdict={}
    for item in list:
        split = item.split(':')
        optionsdict[split[0].strip()] = split[1].strip()
    return optionsdict

So let’s try this out with our messages and see what we get:

>>> reload(serialhandler)
<module 'serialhandler' from 'serialhandler.py'>
>>> serialhandler.monitor()
Monitoring serial port /dev/ttyAMA0
{'tempdev': 'ds18x', 'temprom': '0x28f0d3a7040000cd', 'RX_RSSI': '-68', 'nodeid': '87', 'tempasc': '22.5625'}
{'tempdev': 'ds18x', 'temprom': '0x28f0d3a7040000cd', 'RX_RSSI': '-69', 'nodeid': '87', 'tempasc': '22.6250'}

Excellent! Now we can process as we see fit. What we’re going to do in this case is create a table in our CuPID database specifically for remote data. We do some message typing, data massaging, and scaling, which we ran through in another post. The moral of the story is that as messages are received, they’re logged nicely in a ‘remotes’ table with a timestamp and the messagetype, and as other IO are read and processed, the values are retrieved from the table and inserted into our IO. Our daemon ensure it runs continuously, so we don’t need to worry about it any more.

We’ve programmed our NodeID to 5, and when we open up our motes page, we see that we are in business:

Our Motes show up nicely in our table. We've got a temperature sensor and a battery monitor configured.
Our Motes show up nicely in our table. We’ve got a temperature sensor and a battery monitor configured.

Now, you notice that we’ve configured an analog input, which is attached to a voltage divider which we’re using to monitor battery voltage. We’ll need to scale the input, and while we’re at it we’ll give our inputs handy names as well. We add our scale option manually via phpliteadmin in the metadata table (we’ll add this to our io page later), and add the names directly in our IO table. Our scale shows up, and we see our battery monitor is at 4.995. We’ve got it on USB so far, but we’ll verify once we put it back on 9V. Our voltage divider (1M and 470k) puts 9V at 2.88V, so we’ll be able to cover the range just about right.

Our scaled and named motes in our IO table, alongside our local IO. Our Battery Monitor comes in where it should on USB at 5V.
Our scaled and named motes in our IO table, alongside our local IO. Our Battery Monitor comes in where it should on USB at 5V.

Now we pop on over to our log viewer to make sure we are logging:

After naming and scaling, our Mote temperature battery monitor show up nicely. We're ready to test!
After naming and scaling, our Mote temperature battery monitor show up nicely. We’re ready to test!

We’re golden. Now on to make the actual sealed up unit and do some testing.

Making the Mote

Now, we made the basic box we’ll show here in another post just to see how everything would fit. We really only need to add a resistor and a couple terminals for our 1Wire sensor, and a couple resistors for our voltage divider battery sensor. We buy loads of these waterproof 1Wire sensors, and typically wire them up to 8P8C connectors for use on Cat5 sensor networks, but today we’re going to put a few push on terminals to couple with headers for easy connections.

First, we insert our cord grip, for watertight installation of our wire entry point. A 1/2″ spade bit does the trick nicely.

Insertion of our cord grip. A simple hole and insertion of our screw in connection, and we have IP-rated insertion of our sensor cable ready to go.
Insertion of our cord grip. A simple hole and insertion of our screw in connection, and we have IP-rated insertion of our sensor cable ready to go.

Next, we put our Moteino to a protoplate, solder the pins, and make some connections. As we did on our breadboard, we add a pull-up to the 3.3V line, and connect the data pin to A0, which corresponds to pin 14 in our Arduino sketch:

Attaching our Moteino to a protoboard makes it much easier to connect sensors, connectors, and IO. It gives us power rails and an easy way to mount it too!
Attaching our Moteino to a protoboard makes it much easier to connect sensors, connectors, and IO. It gives us power rails and an easy way to mount it too!

We also connect three header pins to the board so that we can connect our 1Wire sensor or sensors semi-permanently. Finally, we put our battery into place, and seal it all up:

Putting everything together in our box, step by step.

Everything looks fine, and upon connection, our monitor happily reports that we’re reading as scheduled. To have a little fun, let’s make sure this thing does seal properly.

Bath Time

So these enclosures have an IP rating of 65. What this means is, from the definition of the standard, that:

  • The enclosure is dust tight
  • Water projected by a nozzle (6.3 mm) against enclosure from any direction shall have no harmful effects (15 minutes test duration)

We put this to the test. See the video below.

Reference

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

Explanation and installation here:

Your CuPID as a PLC: Modbus Client LED Op Amp Monitoring

Our Demo circuit: An LED Driver

In this example, we’re going to set up a simple constant current source for an LED using an op amp and verify that we’re doing a good job of maintaining constant current as the voltage supply changes.

How it works

The really short version of how an op amp functions in most applications is simple: the op amp will vary voltage output to attempt to bring the two input terminals to the same voltage. Depending on the circuit configuration, this can result in inverting or non-inverting amplification, signal buffering, and all sorts of other neat things.

In the setup we’ll use, shown below, we set the input to the positive terminal using a couple of potentiometers, so that we can achieve the full range offered by the regulator, but still keep current low to avoid overheating it. The other op amp input terminal is connected above a resistor in line with our LED. This way, it directly measures current as a voltage, ILED*Rsense. The op amp controls the current by varying voltage on the gate of an n-channel mosfet. As a result, by tuning the input voltage on the op amp with our potentiometers, we set our target current. With a sense resistor of 10 ohms and a target current of 300mA, for example, we’d have a sense voltage of 3V. Hence, by setting our input voltage on the op amp to 3V, we get 300mA if the op amp does it’s job (this depends on a few other details).

The beauty of the op amp for this application is that it will maintain even very low current levels, which is very very hard to do by controlling only voltage. The diode’s IV curve is exponential, so dialing it in is very hard, and one reason constant current sources are used. Another good reason is that these characteristics change as the devices heat up. Because the light produced is directly related to current (the number of electrons reaching an LED’s junction is proportional to the number that recombine and produce light), the current is the correct quantity to control.

The circuit

We’ll use a voltage regulator with our pots to keep our op amp input voltage constant as we vary the supply voltage. We’re not showing it here, but our power supply to the op amp is +/-12V. We’ll get around to converting this to single supply at some point. We also throw in a current limiting resistor just in case we rail the op amp (maximum output) so we don’t nuke the LED.

Constant Current source for LED using an op amp

Putting it all together

Now let’s get this breadboarded up. We’ll use a few things we have laying around: a 2N7000 MOSFET, an LM341 op amp, a mic2937 3.3V LDO regulator, a Cree XPE blue LED, and a couple 5k pots. This means that if we have the pots set to get max input (R1=0, R2=5k), we’ll get 3.3V / 10ohm = 0.33A. This puts it comfortably in range of the LED’s spec. We’ll have a maximum supply voltage of 12V. With a drop on the LED of 3.5V at maximum current, we have about 8.5V to work with, ignoring the sense resistor and any FET on resistance. This means 28ohms, so 27 is close enough. Our voltage divider uses a 4.7K and a 22K resistor, so our scaling ratio for out Vs input is about 5.7. At about Vs=6.9V, our input reads within about 50mV of our voltmeter, which we can live with for now and calibrate later if we see fit.

We just lost a variable voltage power supply and the replacement is in the mail, so we’ll muddle through with a fixed 12V source power an adjustable switch-mode power supply. Here what we’re looking at. I didn’t clean up for you.

Our test setup, with the important bits labeled.
Our test setup, with the important bits labeled.

We started taking data, and then realized that the UE9 has the unfortunate limitation that the analog inputs are only +/-5V. We’ve got a T7 with a range of +/-10V, but we find the UE9 makes a more reliable MB TCP server. To get around the input voltage limitation, we’ll throw the inputs into a voltage divider. And, instead of having to remember the scaling, we’ll code it in as an option for the input.  All we need to do is add the ‘scale’ keyword to the options field in the mbtcp table. So, for example, for a 4:1 voltage divider, we’d simply add ‘scale:4’ to the options field and be set. This way we can measure up to about 20V on our 5V input. We go ahead and take care of that:

We set out scaling options to allow the use of a voltage divider with output of scaled values.
We set out scaling options to allow the use of a voltage divider with output of scaled values.

We jump over to our dataviewer, and we can see that our scaling has indeed been reflected in the read values (notice the jump from 5 to 20 on the axis where the scaling changed):

Out scaling change is clearly visible in our data viewer. The value jumps from 5 to 20 where we added in our factor of 4 scaling.
Out scaling change is clearly visible in our data viewer. The value jumps from 5 to 20 where we added in our factor of 4 scaling.

Alright, so on with it. We label our inputs and start with our LED supply voltage at about 7V and our op amp input set at 150mV , giving us about 15mA on the LED. Our opamp tracks nicely, matching voltage on the input.

Supply Voltage Tolerance

The first test is to vary the input voltage. The point of this setup is to compensate for Vs variation while keeping constant current, so this is the important test.  So we vary slowly up to the max, ensuring we retain the same sense voltage, and then come back down again:

Varying supply voltage and maintaining constant current.
Varying supply voltage and maintaining constant current.

We notice that at about 3.25V, the op amp output maxes out our analog voltage input. What this means is that the op amp is trying its hardest to compensate and match input voltages, and is having to increase the output voltage to do so. At some point, it will be unable to do so, and it will saturate or ‘rail’ at its maximum output value. Before we add a voltage divider and scale our input for this value, we can’t see how high it can get, but we expect it to get somewhere near its high rail voltage of ~12V. Now, taking a look at our circuit, we can see that a minimum supply voltage of 3.5V for this current makes sense. We’re putting 3.5V supply in at about 20mA. At this current, the forward voltage on the LED is about 2.75, leaving us 0.75V to work with. Our combined sense and current limiting resistance is about 37ohms. Dividing our 0.75V by 37 ohms gives us 20mA. It all makes sense!

Stability

The next important criterion is stability. At a fixed condition, how stable will the current remain, should we want to use the LED signal for measurement purposes? We peg the current at about 50mA at 12V, a pretty typical condition for our envisioned application. Things look quite nice:

Steady-state current output (shown here as voltage inputs to op amp). The condition here is roughly 50mA at 12V supply voltage.
Steady-state current output (shown here as voltage inputs to op amp). The condition here is roughly 50mA at 12V supply voltage. Very little noise, and systematic. Against the resolution of the ADC, this appears to be measurement error on our analog inputs.

Dumping the data and running some statistics, we come up with the following plot and data:

Statistics on our constant current stability test. Measurement is within the noise of the ADC, so our op amp is certainly doing its job. This comes out to something like 0.01% error. Not bad.

The takeaways are the following:

  • Standard error on all signals are within the resolution of the ADC. Within error, they are constant. This explains why the noise on the input signal and sense measurement are the same.
  • Error is low on all measurements: <0.01%. This is suitable for most measurement techniques.
  • Offset voltage is low, at 660uV. This corresponds well to the specified offset voltage of 1mV for the op amp

Summary

Our op amp did great, as did our modbus client. While we need to adapt our design to single-supply operation, the basics are here and appear to work for our current requirements (pun intended).

Join us next time, as we apply these principles to reading a photodiode, and eventually as we control and measure our photodiode for a detection tool!

Reference

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

Explanation and installation here:

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?

Javascript

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.
Python

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!

Reference

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

Explanation and installation here:

Your CuPID as a PLC: Modbus TCP Client

The PLC Pi as a Communicator

As we discussed in a previous post, one of the most important  functions of a PLC is as a communication swiss army knife. A given control system might have devices that speak half a dozen or more languages on as many interfaces that need to be read and written. If the central control device cannot interface with a device, different hardware must be specified to resolve compatibility. Obviously, the fewer languages your PLC speaks, the fewer options you have when it comes to selecting instrumentation and acquisition hardware.

The Modbus Protocol

One of the most common formats in use in the industry is Modbus, which has been around since 1979. It’s a simple and somewhat archaic protocol, but one that you’ll find everywhere as a least common denominator for inter-device communication. While it’s quite limited in structure, the fact that it is well-defined makes it an easy drop-in format that’s easy to configure.

We won’t get into the nitty gritty of frame and bit by bit message format, but the protocol has the following properties:

  • A Modbus transaction has two participants: a Master (Client) and Slave (Server)
  • Members on a multi-drop Modbus network are identified by a Node number (address), and in the case of Modbus TCP/IP, an IP Address. In the case of TCP/IP, the node number is unnecessary except in unusual cases where the Slave acts as a gateway for multiple devices.
  • Data exist in a Modbus slave in banks of data locations referred to as Registers
  • Data transactions consist of a Function Code, which specifies a read or write transaction, as well as which bank of registers to act on, a specific register number, and any data required to complete the request
  • There is no way to encode metadata, except in adhoc systems. Datamaps must be defined outside of the protocol itself. Most devices will have a map that says “data A is in register X, in format Y”
Registers and Function Codes
Name Registers Datatype Mode Read FC Write FC
Coils 000001-065535 bit RW 1 5
Discrete Inputs 100001-165535 bit R 2 -
Input Registers 300001-065535 bit R 4 -
Holding Registers 400001-165535 bit RW 3 6

The Physical Layer

The above defines the communication format, i.e. where the bytes and bits go. It does not define, however, how that information is physically communicated. Typically, Modbus is employed in serial formats such as RS232, RS485, and over TCP/IP. The first two are what might be considered legacy, in particular RS232, but they are still in widespread usage in industry today. TCP/IP is the go-to format for most networks today, and what new designs are built around, at least as far as Modbus is concerned. In general, where the overhead of the TCP/IP layer is not a killer for transfer efficiency or power considerations, it’s a good way to go, if for no other reason that so many network infrastructures are built on it.

Although it is possible to interface the Pi with devices on RS485 and RS232, using level-shifting transceivers depending on the voltage levels of the host devices, what we will cover here is Modbus TCP/IP, for the fact that it is the most widely used moving forward, and that the standard is so well defined. Plus, we’ve got some great toys laying around that talk it, which make great demos!

How does Pi talk Modbus?

There are many options available for speaking modbus on  a linux box. We’ll demonstrate a Python implementation, for the reasons that it is the default language for Pi projects and because it’s what we write all of our server-side code in here. We use a nice little library called Pymodbus, which is quite easy to use. On top of it, we build in some error-handling and user-friendliness. You can find that here. Thumbing down a bit, you can find the function below, which is the workhorse for our MB TCP/IP reads:

def readMBcodedaddresses(clientIP,address,length=1)

The function takes a server IP, register, and read length, chooses the appropriate function code, and returns the read value(s). Some basic coded error functions are included.

>>> import netfun
>>> netfun.readMBcodedaddresses('192.168.1.202',400001,2)
ReadRegisterResponse (2)
{'message': 'status ok', 'values': [16544, 20890], 'statuscode': 0}
>>>

We’re working on an analogous write routine.

One thing to keep in mind, however, is that each transaction has an overhead involved with establishing the connection. If you are carrying out multiple reads, it’s much more efficient to intelligently group register reads into blocks. We’ve written a young piece of code that will do this for you, given you have a datamap that has the correct structure for it. The main function is datamaptoblockreads, and interprets an sqlite table of reads, groups them into blocks, and parses the data. This ends up saving loads of time for large read operations. We’ll cover this later.

What to do with it?

So we’ve got some server-side python that reads clients on the network. What do we do with it? Well, a few things:

  • Read data from LAN/WAN host devices
  • Log and database it
  • Expose it on a web server
  • Accept and process write requests

We’ll explore the topics above in a miniseries:

Reference

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

Explanation and installation here: