Arduino Sensor Network

Previously, we looked at creating a Low Power Custom Arduino Sensor Board for use in a sensor network. Now, let’s look at writing the software for our sensor network using that custom board. We will revisit the bathroom occupancy sensor as an example.

Low Power Using Interrupts

Last time we looked at optimizing power by sleeping the Arduino and waking it only when a door’s state changed. To do this, we used the pin change interrupt on pins 2 and 3. Unfortunately, we can only monitor change on those pins when the processor is in idle mode which doesn’t offer us max power savings. It would be nice to put the processor into its highest power savings mode: deep sleep. In deep sleep, those pins can still use interrupts but instead of interrupting on change, it would interrupt on a single state of HIGH or LOW. To use this type of interrupt we would have to remove the interrupt after it fires and then add it back to trigger on the opposite state every time the processor was woken. This is doable but there is a simpler solution.

The Watchdog Timer (WDT) is a timer that runs on microcontrollers as a safety feature. Its purpose is to notify the processor if a fault or exception occurs. Say for instance, that you have code in the main execution loop that you know takes no more than 100ms to execute. You could set the WDT to interrupt just over 100ms. Then at the end of every execution loop reset the WDT. Now if the WDT interrupt is ever reached, you know that the WDT timed out meaning the code took longer than expected to execute. You can then handle the failure accordingly in the interrupt callback.

We are going to use the WDT a little differently than its intended use. If we set the WDT to interrupt every second, then we can put the processor into deep sleep and it will wake up in second intervals. Every time it wakes up, we can check the doors and transmit their state if it has changed. This may not be ideal for optimal power savings but it doesn’t cost much more power and it allows us to have a more general application for sensing and reporting anything. Our Arduino will wakeup every second, check some sensors, and then report their state if they changed. Using this model, we can have the sensor board be more than just a bathroom door detector. It could be placed around the office and report temperature, humidity, brightness, motion, etc.

Let’s create a new Arduino project and setup the WDT.

// Import the interrupt library
#include <avr/interrupt.h>

volatile int __watch_dog_timer_flag = 1;

// Define WDT interrupt callback
ISR(WDT_vect)
{
  __watch_dog_timer_flag = 1;
}

void setup()
{
  // Disable processor reset on WDT time-out
  MCUSR &= ~(1<<WDRF);

  // Tell WDT we're going to change its prescaler
  WDTCSR |= (1<<WDCE);

  // Set prescaler to 1 second
  WDTCSR = 1 << WDP1 | 1 << WDP2;

  // Turn on the WDT
  WDTCSR |= (1 << WDIE);
}

void loop()
{
  if (__watch_dog_timer_flag == 1) {
    __watch_dog_timer_flag = 0;

    // do things here ...
  }
}

First, we create a flag variable that we use to know which interrupt was fired. We use the volatile keyword to let the compiler know that this variable might change at any time. This is important for variables being modified within interrupt callbacks and the main execution loop. Next, we define the interrupt callback using the ISR, Interrupt Service Routine, function. We tell the ISR which interrupt callback we’re defining, WDT_vect is for the Watchdog Timer Interrupt Vector. The only thing we need to do Inside the interrupt callback is set the flag.

Next, we setup the WDT using some register bit manipulation. The MCUSR register is the processor’s status register and allows us to reset the processor when the WDT times out. We don’t want this to happen so we use the bitwise & operator to set the WDRF, Watchdog Reset Flag, bit to 0. Then, we configure the WDT Control Register, WDTCSR. Setting the WDCE bit tells the processor that we are going to change the timer prescaler. Then, we set the timer prescaler with the WDP1 and WDP2 bits so the WDT will time-out around 1 second. Now, we can enable the WDT by setting the WDIE bit. You can find out more about these registers in the datasheet. Finally, in the execution loop, we check to see if the flag is set, meaning the WDT has triggered. If it has triggered, we reset the flag and execute the application specific code.

Revisiting the bathroom occupancy detector, the sensor board in charge of monitoring the downstairs bathrooms has to sense the input from 2 reed switches on the doors. We will use pins D2 and D3 for the reed switches. We also want to activate the internal pull-up resistor which adds a resistor to power inside the chip. This gives our door pins a default state if the door is not closed.

byte leftDoorStatus = 0;
byte rightDoorStatus = 0;

void setup()
{
  // WDT init ...

  pinMode(2, INPUT);
  digitalWrite(2, HIGH);

  pinMode(3, INPUT);
  digitalWrite(3, HIGH);
}

void loop()
{
  if (__watch_dog_timer_flag == 1) {
    __watch_dog_timer_flag = 0;

    byte left = digitalRead(2);

    if (leftDoorStatus != left) {
      leftDoorStatus = left;
    }

    byte right = digitalRead(3);

    if (rightDoorStatus != right) {
      rightDoorStatus = right;
    }
  }
}

Here, we added two global status variables. Then, we set the pins 2 and 3 as INPUT and turn on their internal pull-up resistors using digitalWrite(x, HIGH);. In the loop function, we check and compare the doors' status with the global status. If the status has changed we set the global variable. Now, we can use the nRF24 board to communicate these changes to the hub.

#include <SPI.h>
#include <Mirf.h>
#include <nRF24L01.h>
#include <MirfHardwareSpiDriver.h>

// ...

void setup()
{
  // ...

  Mirf.csnPin = 10;
  Mirf.cePin = 9;
  Mirf.spi = &MirfHardwareSpi;
  Mirf.init();
  Mirf.setRADDR((byte *)"bath1");
  Mirf.payload = 32;
  Mirf.config();
}

Make sure to include the proper libraries. We can download the Mirf library and place it into our Arduino libraries folder. Setup the Mirf by setting the csnPin and cePin, which are on pins 10 and 9 respectively, telling it to use the hardware SPI, setting the address as bath1, and the payload as 32 bytes. Now in the execution loop we can transmit the data when a status has changed.

const String rightDoorID = "E1MLhY2yhH";
const String leftDoorID = "bEOr5qhMHY";

void loop()
{
  if (__watch_dog_timer_flag == 1) {
    __watch_dog_timer_flag = 0;

    byte left = digitalRead(2);

    if (leftDoorStatus != left) {
      leftDoorStatus = left;
      sendDataWithIDAndStatus(leftDoorID, leftDoorStatus);
    }

    byte right = digitalRead(3);

    if (rightDoorStatus != right) {
      rightDoorStatus = right;
      sendDataWithIDAndStatus(rightDoorID, rightDoorStatus);
    }
  }
}

void sendDataWithIDAndStatus(String id, byte status)
{
  byte doorStatus[12];
  id.getBytes(doorStatus, 11);
  doorStatus[11] = status;

  Mirf.setTADDR((byte *)"tbhub");
  Mirf.send(doorStatus);
  while(Mirf.isSending()) ;
  Mirf.powerDown();

  free(doorStatus);
}

First, we add two IDs for our door sensors. These IDs correspond to their respective ID in the cloud storage service we are using to store the data (more on this later). When the status has changed we call sendDataWithIDAndStatus(id, status) which combines the ID and status of the door into a byte array and uses Mirf to transmit the array to the hub, tbhub. We wait for transmission to finish and then tell the Mirf board to sleep.

The last thing we have to do is sleep the processor after the application code has executed.

#include <avr/power.h>
#include <avr/sleep.h>

void loop()
{
  if (__watch_dog_timer_flag == 1) {
    __watch_dog_timer_flag = 0;

    // Application code ...

    enterSleepMode();
  }
}

void enterSleepMode()
{
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  sleep_enable();
  sleep_mode();

  sleep_disable();
  power_all_enable();
}

Call enterSleepMode() after our application code in the main loop. This function sets and enables the sleep mode then tells the processor to enter sleep mode with sleep_mode(). When the processor wakes up from the interrupt, code execution begins where it left off, disabling sleep and turning on power for all peripherals.

A Library to Simplify

We have provided an Arduino library that we can use to make this much simpler. Add the thoughtbot directory into the Arduino libraries directory and restart the Arduino software.

The library provides the TBClient class and a wrapper file TBWrapper.

The TBClient class abstracts the communication to the hub. Initialize a client class by calling TBClient client((byte *)"cname", 32);. This will initialize the Mirf software. The first parameter is the name of the client device. This name will be used to receive transmissions meant just for this board. It’s very important that this name be 5 characters long or else the wireless library won’t work. The second parameter is the size of the transmission payload in bytes. The max is 32 bytes which we set above even though we might not use all that. TBClient also provides a sendData(byte *address, byte *data) function for transmitting. It takes in the 5 character address of the device to transmit to and the byte array of data to transmit.

TBWrapper is a file that wraps the standard Arduino setup() and loop() functions to setup the WDT and put the processor in deep sleep. If we wanted custom sleep and interrupt logic other than what we did above, we could remove this file. Keeping this file will simplify the code so that we can concern ourselves only with our application. With TBWrapper, use clientSetup() and clientLoop() instead of setup() and loop() respectively. Inside clientSetup(), we can setup any pins or modules we need for our sensing application. clientLoop() will be executed about every second when the processor comes out of sleep. In here, we should check our sensors and transmit their data if any have changed.

To use this library, create a new file with the Arduino software. In the menu, under Sketch select Import Library... and pick thoughtbot. Also import the Mirf and SPI libraries. The final code after refactoring the above code to use the libraries will look like this:

#include <SPI.h>
#include <Mirf.h>
#include <nRF24L01.h>
#include <MirfHardwareSpiDriver.h>
#include <MirfSpiDriver.h>

#include <TBClient.h>
#include <TBWrapper.h>

const String rightDoorID = "E1MLhY2yhH";
const String leftDoorID = "bEOr5qhMHY";

TBClient client((byte *) "bath1", 32);

byte leftDoorStatus = 0;
byte rightDoorStatus = 0;

void clientSetup()
{
  pinMode(2, INPUT);
  digitalWrite(2, HIGH);

  pinMode(3, INPUT);
  digitalWrite(3, HIGH);
}

void clientLoop()
{
  byte left = digitalRead(2);

  if (leftDoorStatus != left) {
    leftDoorStatus = left;
    sendDataWithIDAndStatus(leftDoorID, leftDoorStatus);
  }

  byte right = digitalRead(3);

  if (rightDoorStatus != right) {
    rightDoorStatus = right;
    sendDataWithIDAndStatus(rightDoorID, rightDoorStatus);
  }
}

void sendDataWithIDAndStatus(String id, byte status)
{
  byte doorStatus[12];
  id.getBytes(doorStatus, 11);
  doorStatus[11] = status;
  client.sendData((byte *)"tbhub", (byte *)doorStatus);
  free(doorStatus);
}

The Hub

The hub, our Arduino Yún, also has an nRF24 board attached and is receiving the transmissions. It will post the sensor data to an internet service so we can access that data from anywhere. We decided to use Parse as the internet service because of its ease to use and large data capacity for the free tier.

Let’s look at how we can receive data from our sensor board and post it to the cloud.

#include <SPI.h>
#include <Mirf.h>
#include <nRF24L01.h>
#include <MirfHardwareSpiDriver.h>
#include <MirfSpiDriver.h>

#include <Bridge.h>
#include <Process.h>

void setup()
{
  Mirf.spi = &MirfHardwareSpi;
  Mirf.init();

  Mirf.setRADDR((byte *) "tbhub");
  Mirf.payload = 32;

  Mirf.config();

  Bridge.begin();
}

Here, we are setting up the Mirf library by giving it the name of our device, tbhub, and the payload size, 32 bytes. The Bridge.begin(); call is setting up the Arduino to be able to talk to the on-board Linux computer. Now we can monitor for received data in the loop() function.

void loop()
{
  if (Mirf.dataReady()) {
    byte data[32];
    Mirf.getData((byte *) &data);
    String id = String((char *)data);
    sendData(id, data[11]);
  }
}

When we receive data, we extract the sensor ID from the first bytes of the string and send it along with the status byte to the Parse API.

void sendData(String id, byte value)
{
  Process curl;
  curl.begin("curl");
  curl.addParameter("-k");
  curl.addParameter("-X");
  curl.addParameter("POST");
  curl.addParameter("-H");
  curl.addParameter("X-Parse-Application-Id:YOUR-APPLICATION-ID");
  curl.addParameter("-H");
  curl.addParameter("X-Parse-REST-API-Key:YOUR-PARSE-API-KEY");
  curl.addParameter("-H");
  curl.addParameter("Content-Type:application/json");
  curl.addParameter("-d");

  String data = "{\"sensor\":{\"__type\":\"Pointer\",\"className\":\"Sensor\",\"objectId\":\"";
  data += id;
  data += "\"},\"value\":";
  data += value;
  data += "}";

  curl.addParameter(data);
  curl.addParameter("https://api.parse.com/1/classes/SensorValue");
  curl.run();
}

Process is a class available on the Arduino Yún that sends a command to the Linux computer for execution. Unfortunately, the string parameter in the addParameter(String) method, must not contain any spaces, leaving the code to look messy and repetitive. We are using curl to POST the new sensor status to a Parse object called SensorValue. The string identifiers for each door on the sensor board correspond to a Sensor object on Parse. Above, we are creating a new SensorValue object in Parse that points to the appropriate Sensor object.

This code and the code for the client can be found in the GitHub repository.

Conclusion

Now we have the code to make our sensor board run, and with that we can start sensing and reporting anything we can imagine. The hardware and software is all open source, so make a sensor network at your office or home and report back to us with your awesome creations!

Hound automatically reviews Ruby, JavaScript, and CoffeeScript code in your GitHub pull requests and comments on style violations. It is free for open source repos and $12/month per private repo.