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!