collage

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:

4 thoughts on “Adventures in Moteino: Remote Temperature Monitor”

Leave a Reply

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