POF 03 Wireless light sensor
- POF 03 Wireless light sensor
- Purpose
- Goal
- What You'll Need
- Introduction
- 1. Setting up the Central Node
- 2. Preparing the Light Sensor Node
- 3. Software for the Light Sensor Node
- 4. One More Configuration Issue
- 5. How Does It Work?
- 6. Making the Node More Responsive
- 7. Reducing Power Consumption
- A) Doing Nothing
- B) Turning the Radio Off
- C) Can We Do Better?
- What's Next
Purpose
We're going to set up everything needed for a basic Wireless Sensor Network (WSN).
Goal
To read out a battery powered light sensor over wireless:
What You'll Need
- 1 x POF base + JeeNode - see POF 01 Foam base construction
- 1 x LDR sensor - a light-dependent resistor
- 1 x central node - a JeeLink or JeeNode to receive the data
Introduction
One of the main uses for JeeNodes is to build Wireless Sensor Networks, sending back readings from different rooms in the house for example. Apart from the hardware needed for each node, this also requires a wireless connection and the proper software to manage those connections. At least two different bits of software are needed: the collecting software on the central node, and the software running on each of the remote nodes.
This example will set up the basic infrastructure using a minimal configuration of a central node with a single remote node. Again, this is not strictly a POF in that it does not really require a foam base, but it's setting the stage for more elaborate configurations.
1. Setting up the Central Node
The central node is very easy to set up because JeeNodes come pre-loaded with all the required software, in the form of the "RF12demo" sketch. The only thing that needs to be done is to hook this node up via USB, and make a few configuration choices:
- What frequency band to use: normally this will be 868 MHz in Europe and 915 MHz in the US.
- What net group to use - these groups determine which nodes can talk to each other: nodes in the same group can, while nodes in different groups cannot talk to each other.
- Picking a unique ID for each node - these must be in the range 1..30, with 1..26 normally used for the remote nodes, and node 30 being the central node, by convention.
Let's say you've decided to use 868 MHz, group 5, node 30 for the central node, and that you have hooked up the central node and can communicate with it using either the Arduino IDE's built-in serial console, or some other terminal emulator.
The first thing you should see when connecting is a detailed help message, something like this:
[RF12DEMO] A i1 g212 @ 433 MHz
Available commands:
<nn> i - set node ID (standard node ids are 1..26)
(or enter an uppercase 'A'..'Z' to set id)
<n> b - set MHz band (4 = 433, 8 = 868, 9 = 915)
<nnn> g - set network group (RFM12 only allows 212)
<n> c - set collect mode (advanced, normally 0)
t - broadcast max-size test packet, with ack
...,<nn> a - send data packet to node <nn>, with ack
...,<nn> s - send data packet to node <nn>, no ack
<n> l - turn activity LED on PB1 on or off
Remote control commands:
<hchi>,<hclo>,<addr>,<cmd> f - FS20 command (868 MHz)
<addr>,<dev>,<on> k - KAKU command (433 MHz)
Current configuration:
A i1 g212 @ 433 MHz
Now enter the following command and hit return:
8b 5g 30i
As result, the new settings will be shown:
^ i30 g5 @ 868 MHz
These settings are permanently stored in the JeeNode's EEPROM and are retained even when powered off.
Good. You now have a properly configured central node, which will receive all packets sent to it in group 5 on 868 MHz. If you leave this running for a while, you may see lines such as:
? 9 66 117 72 161 211 11
These are spurious receptions with incorrect checksums, they are caused by random noise in the 868 MHz frequency band.
Once you get a real sensor node up and running, sending real packets, you'll see lines of the form:
OK 33 149
-> ack
This indicates a valid packet has been received, and an acknowledgement has been sent back to it.
2. Preparing the Light Sensor Node
The sensor node we're setting up is very simple - it has a light-dependent resistor (LDR) connected to the AIO pin of port 3, and that's it:
As you can see above, the LDR needs to be connected between AIO and GND. The idea is to enable a pull-up resistor in the ATmega, so that it forms a voltage divider with the LDR. When the LDR resistance is low (much light), the AIO pin will be pulled towards ground, and when it is high (little light), the AIO pin will be pulled towards 3.3V by the internal pull-up resistor (that's why it's called "pull-up"!). We use the AIO pin, because that pin is able to measure an analog voltage level between 0 and 3.3V.
3. Software for the Light Sensor Node
The software for the light sensor node consists of three parts:
- The code which reads out the current LDR sensor value.
- The "RF12" library code which takes care of the wireless transmissions.
- A little bit of logic to send measured values periodically to the central node. First, we need to install some extra software: the RF12 and Ports libraries, both written and maintained by JeeLabs. The RF12 library is available here and the Ports library is here. Both are ZIP archives - after unpacking them, you should have two folders, named "RF12" and "Ports", respectively.
Now, you need to find out where the Arduino IDE stores your sketches (this instructions require IDE version 0017 or higher). There should be a "Preferences..." menu entry in the Arduino (it depends on whether you're running on Windows, Mac, or Linux). I'm on Mac and get this window (only top part shown):
So on my setup, the folder is at "/Users/jcw/Documents/Arduino" - yours will be different ... unless you're on a Mac and have the same initials ;)
Ok, next step is to quit the Arduino IDE, because it doesn't see the changes we're about to make without being re-started.
Then, go to the area you just found and create a folder called "Libraries" in it, if it doesn't already exist. Finally, place the RF12 and Ports folders inside the Libraries folder and start up the Arduino IDE again.
Good, you should now be able to use these two libraries in your sketches. As we're about to do.
Create a new sketch, and copy the following text into it via the Arduino IDE editor:
#include <Ports.h>
#include <RF12.h>
Port ldr (3);
void setup () {
// initialize the serial port and the RF12 driver
Serial.begin(57600);
Serial.print("\n[pof66]");
rf12_config();
// set up easy transmissions every 5 seconds
rf12_easyInit(5);
// enable pull-up on LDR analog input
ldr.mode2(INPUT);
ldr.digiWrite2(1);
}
void loop () {
// keep the easy tranmission mechanism going
rf12_easyPoll();
// convert analog 0..1023 readings to a 0..255 light level
byte value = 255 - ldr.anaRead() / 4;
// send measurement data, but only when it changes
rf12_easySend(&value, sizeof value);
}
Now press the "Run" button (the leftmost one with the right-pointing arrow in it). You should see this text:
Binary sketch size: 4274 bytes (of a 30720 byte maximum)
Got that? Good. You have just prepared the software which will be used in the remote node.
4. One More Configuration Issue
Before uploading this sketch to the remote node, you need to do one more thing. As described above, each node needs to use the same frequency band and group, but a different node ID. One way to do this would be to change the ID in the software right before uploading. But this is cumbersome and error prone once you're dealing with a couple of nodes. The approach chosen here is to store the wireless parameters in each node's EEPROM once, and then upload the same code to all of them. That's what the "rf12config()" call does in the above code.
So before uploading the new code, let's configure the EEPROM. This is again done using the RF12demo, which comes pre-loaded on every JeeNode. The process is similar to what we did for the central node - start the node with a console or terminal connected and enter:
8b 5g 1i
The new settings will be shown:
A i1 g5 @ 868 MHz
We've used node ID "1" in this case. Now the EEPROM has been set up, and we can replace the RF12demo with our own sensor node code.
So let's upload it. Connect the remote node to USB (not the central one!):
If you use multiple USB connections, or a JeeLink + JeeNode as setup, *make sure you're talking to the correct one in the Arduino IDE!* It's very easy to mix things up. Doing so is no big deal, but you'll have to restore both nodes to contain their proper sketches.
With the above sketch loaded into the Arduino IDE, and the proper node connected to USB, you can now upload the new code. Again, you should see this text:
Binary sketch size: 4272 bytes (of a 30720 byte maximum) Also, if all is well, the IDE will report "Done uploading."
That's it - your sensor network is now up and running! To see the incoming results, you need to reconnect to the central node. Ah, but wait - if you disconnect the sensor node it will stop running.
The solution, of course, is to switch to battery mode. Disconnect the USB cable and adapter from the remote JeeNode, and connect / turn on the battery pack. Now reconnect the USB cable to the central JeeNode and open a serial console / terminal window to see its ouput.
If all is well, you'll see lines coming in with data:
OK 33 149
-> ack
OK 33 152
-> ack
[etc]
Congratulations - you have just created your own Wireless Sensor Network!
5. How Does It Work?
So what exactly is going on here? Well...
- The central node is listening for packets in group 5 on the 868 MHz frequency band, and reporting each one it receives.
- The remote node reads out its LDR sensor, and sends out a packet every 5 seconds.
- The central node sends an acknowledgement packet to inform the remote node each time data has been properly received.
So every 5 seconds, a data packet is picked up by the central node, and a short "ack" packet is sent back. If a packet is lost or damaged, it will be re-sent.
The remote node is relatively simple, because all the packet transmission and time-out / ack packet handling is taken care of by the three "rf12easy...()" calls, which are part of the "easy transmission" mechanism, as described in this article.
The system works fine, but it's still fairly simplistic:
- First of all, it's a bit sluggish. It takes up to 5 seconds before a change in light levels gets reported to the central node.
- Second, it's not very battery friendly - the node and the radio are always on, consuming around 20 mA. The battery will last only a couple of days.
6. Making the Node More Responsive
The reason that the sensor node is sluggish, is that we've specified a 5-second period in the rf12_easyInit() call. Setting that to 1 second would make the node more responsive, but we might end up sending thousands of packets per hour if the light levels constantly change, even slightly.
We can do better than that. First of all, we're not going to let the RF12 driver determine how often we send packets, but add a periodic timer to the code instead. That allows us to call rf12_easyInit() with argument 0, meaning: once there is new data, send it as soon as possible. Second, we're going to use a moving average, so that quick changes are filtered out, and only slow-moving light level trends are passed on. Where "slow-moving" is to be interpreted as over a period of 5 seconds or so. A quick change from say light level 150 to 151 and then back to 150 will be completely ignored. This approach will still cause a delay for small changes, but big changes will be detected nearly instantly.
One more improvement is to force a packet out every 60 seconds, even if the light levels do not change at all for a long time (at night, for example). That way the central node can easily verify that the node is still running properly.
The code changes required to implement all this are fairly small - here is the complete updated version:
#include <Ports.h>
#include <RF12.h>
Port ldr (3);
MilliTimer readoutTimer, aliveTimer;
word runningAvg;
void setup () {
// initialize the serial port and the RF12 driver
Serial.begin(57600);
Serial.print("\n[pof66]");
rf12_config();
// set up easy transmissions at maximum rate
rf12_easyInit(0);
// enable pull-up on LDR analog input
ldr.mode2(INPUT);
ldr.digiWrite2(1);
// prime the running average
runningAvg = ldr.anaRead();
}
void loop () {
// keep the easy tranmission mechanism going
rf12_easyPoll();
// only take a light level measurement once a second
if (readoutTimer.poll(1000)) {
// keep track of a running 5-second average
runningAvg = (4 * runningAvg + ldr.anaRead()) / 5;
// convert analog 0..1023 readings to a 0..255 light level
byte value = 255 - runningAvg / 4;
// send measurement data, but only when it changes
rf12_easySend(&value, sizeof value);
}
// force a "sign of life" packet out every 60 seconds
if (aliveTimer.poll(60000))
rf12_easySend(0, 0);
}
The code is slightly larger, when you compile it you should get:
Binary sketch size: 4522 bytes (of a 30720 byte maximum)
Now you can upload the sketch and try it out. Always make sure you have the correct code in the Arduino IDE, the correct USB adapter connected, and the correct serial port selected in the IDE.
This version should be far more responsive to large light changes, but will probably send fewer packets than before under calm conditions - light levels do change during the day, but not that fast, usually.
7. Reducing Power Consumption
The second flaw in the remote node is that it's drawing too much current to run for longer periods of time. With a 2000 mAh battery, consuming say around 20 mA, the remote node will be out of juice in just over 4 days of use. That's not very practical.
One solution of course is to simply hook it up to a little power supply. That would work fine in the house, but it'll mean running some wires to the node and permanently occupying a wall outlet.
The other solution is to try and get that battery consumption down. After all, we're really doing nothing most of the time - just waiting for the next second tick to arrive! So the "clever" approach is to try to take advantage of a range of sleep modes built into an ATmega.
Before setting your expectations too high, be aware that truly low power battery use, with months or even years of running time from a few AA cells is a very complex topic. The problem is that the weakest link will determine the actual power consumption: even if you get the consumption down between 1-second measurements, this may be completely irrelevant if the measure/send phase continues to run at 40 mA levels for 10's of milliseconds. There have been some posts on this interesting topic on the weblog, as listed on the Daily weblog topics page, under "Low-power techniques".
Let's just start and see what low-hanging fruit there is to pick...
Our baseline is the code we have so far, which draws 19.3 mA current on average (either version above).
A) Doing Nothing
The easiest way to save on power consumption is to do less. Right now, the sensor node is going through a loop hundreds of thousands of times per second to check if "something happened" and new action needs to be taken. But in this setup, there are only really two sources of change: time passing, and a change in the radio module. Both are always associated with an interrupt.
So the very first step we can take is to switch to the ATmega's "idle" mode,
which halts processing and reduces power consumption until the next interrupt
occurs. We need to add an "#include
void loop () {
// switch to idle mode while waiting for the next event
set_sleep_mode(SLEEP_MODE_IDLE);
sleep_mode();
// keep the easy tranmission mechanism going
[etc...]
The effect of this simple change, is that the loop will "hang" until there is an interrupt. The reasoning is that we just went through the loop, and took whatever action was needed. So now we simply wait for a change before checking the various conditions again. The nice thing about this change is that it does not affect the rest of our code, it just slows it down a bit and stops wasting energy doing nothing - literally!
This simple change reduces the current to 14.7 mA. Good, but not good enough, clearly.
B) Turning the Radio Off
The next thing to consider is to turn the radio off. Right now, the radio is always in receive mode - when not sending out a data packet, that is. But there's no point listening to incoming packets when no ack's are expected. And the radio is a major power consumer, even in receive mode.
This takes a bit more work. We can't just switch the radio off after sending - first of all, the radio is probably still busy sending out the actual bytes, but more importantly, we need to wait for the acknowledgement packet to come in.
The trick is to look at the return value from rf12_easyPoll(), this will only be zero when there is nothing left to do. There are two cases, both will allow us to switch the radio off: either the ack has been received and we're done, or we've re-sent the data 8 times and still have not received an ack - in which case the easy transmission mechanism will also give up and stop sending packets.
We do have to be careful about turning the radio back on again when we're going to send data. Again, there is a return value we can use: if rf12_easySend() returns a non-zero value, this means it will actually send out a new packet - else the data didn't change, so no new transmission will be started.
The main loop now looks like this:
void loop () {
// switch to idle mode while waiting for the next event
set_sleep_mode(SLEEP_MODE_IDLE);
sleep_mode();
// keep the easy tranmission mechanism going
if (radioIsOn && rf12_easyPoll() == 0) {
rf12_sleep(0); // turn the radio off
radioIsOn = 0;
}
// only take a light level measurement once a second
if (readoutTimer.poll(1000)) {
// keep track of a running 5-second average
runningAvg = (4 * runningAvg + ldr.anaRead()) / 5;
// convert analog 0..1023 readings to a 0..255 light level
byte value = 255 - runningAvg / 4;
// send measurement data, but only when it changes
byte sending = rf12_easySend(&value, sizeof value);
// force a "sign of life" packet out every 60 seconds
if (aliveTimer.poll(60000))
sending = rf12_easySend(0, 0); // always returns 1
if (sending) {
// make sure the radio is on again
if (!radioIsOn)
rf12_sleep(-1); // turn the radio back on
radioIsOn = 1;
}
}
}
This reduces the current to 3.3 mA. Not bad, our sensor node will now run for about 25 days on those same AA batteries.
C) Can We Do Better?
Yes, we can - but this is where the law of diminishing returns comes in, full force: it takes more and more trickery to deal with all possible situations. And only when all cases have been optimized, can substantial further power reductions be achieved. Keep in mind that the power usage varies across a range of some 4 orders of magnitude - and both extremes are needed: the minimal power down mode to pass most of the time, as well as the high-power mode where the radio is on and transmitting a packet at maximum power. The trick is essentially to keep the high-power modes as brief as possible under all circumstances.
The bottom line is that the ATmega uses only a few µA when powered down, and so does the radio. Achieving these levels requires turning off things like the ADC and the LDR pull-up when going into power down mode. The challenge is to do this as often as possible, without losing track of time (especially once you start messing with system clock dividers and watchdogs). The margin of error becomes a lot smaller when pushing for minimal power consumption - if you decide to use the radio as "watchdog" to wake up again after a certain amount of time, then you better not lose that wake-up call under any circumstance, or the system will simply end up sleeping forever (until reset)!
With just the above two steps, we have achieved a 6-fold improvement in battery life. As it turns out, one more change will reduce the idle (not average!) current of this node to about 20 µA. This is described in a separate weblog post and requires diving a bit deeper into the low-power innards of an ATmega.
Here are the final 80+ lines of code for our Wireless Sensor Node, with all three power reduction tricks:
#include <Ports.h>
#include <RF12.h>
#include <avr/sleep.h>
Port ldr (3);
MilliTimer readoutTimer, aliveTimer;
word runningAvg;
byte radioIsOn;
void setup () {
// initialize the serial port and the RF12 driver
Serial.begin(57600);
Serial.print("\n[pof66]");
rf12_config();
// set up easy transmissions at maximum rate
rf12_easyInit(0);
// enable pull-up on LDR analog input
ldr.mode2(INPUT);
ldr.digiWrite2(1);
// prime the running average
runningAvg = ldr.anaRead();
// start with the radio on
radioIsOn = 1;
}
static void lowPower (byte mode) {
// disable the ADC
byte prrSave = PRR, adcsraSave = ADCSRA;
ADCSRA &= ~ bit(ADEN);
PRR &= ~ bit(PRADC);
// go into power down mode
set_sleep_mode(mode);
sleep_mode();
// re-enable the ADC
PRR = prrSave;
ADCSRA = adcsraSave;
}
static void loseSomeTime (word ms) {
// only slow down for longer periods of time, as this is a bit inaccurate
if (ms > 100) {
word ticks = ms / 32 - 1;
if (ticks > 127) // careful about not overflowing as a signed byte
ticks = 127;
rf12_sleep(ticks); // use the radio watchdog to bring us back to life
lowPower(SLEEP_MODE_PWR_DOWN); // now we'll completely power down
rf12_sleep(0); // stop the radio watchdog again
// adjust the milli ticks, since we've just missed lots of them
extern volatile unsigned long timer0_millis;
timer0_millis += 32U * ticks;
}
}
void loop () {
// switch to idle mode while waiting for the next event
lowPower(SLEEP_MODE_IDLE);
// keep the easy tranmission mechanism going
if (radioIsOn && rf12_easyPoll() == 0) {
rf12_sleep(0); // turn the radio off
radioIsOn = 0;
}
// if we will wait for quite some time, go into total power down mode
if (!radioIsOn)
loseSomeTime(readoutTimer.remaining());
// only take a light sensor reading once a second
if (readoutTimer.poll(1000)) {
// keep track of a running 5-second average
runningAvg = (4 * runningAvg + ldr.anaRead()) / 5;
// convert analog 0..1023 readings to a 0..255 light level
byte value = 255 - runningAvg / 4;
// send measurement data, but only when it changes
char sending = rf12_easySend(&value, sizeof value);
// force a "sign of life" packet out every 60 seconds
if (aliveTimer.poll(60000))
sending = rf12_easySend(0, 0); // always returns 1
if (sending) {
// make sure the radio is on again
if (!radioIsOn)
rf12_sleep(-1); // turn the radio back on
radioIsOn = 1;
}
}
}
Welcome to the exciting world of Wireless Sensor Nodes. Thomas Edison, Guglielmo Marconi, Nikola Tesla, and Alessandro Volta could not have imagined in their wildest dreams what you have just accomplished!
What's Next
You've set up a remote sensing node running off batteries and periodically transmitting its measurements to a central node. This can be the starting point for a variety of sensing nodes, with the central node collecting all data, presenting the results visually, and triggering on specific conditions. The basic code won't need to change much, just add more sensors and include the corresponding data in the packets sent out.
The Room Board is a little daughterboard for the JeeNode combining a temperature, humidity, motion, and light sensor.
The easy transmission mechanism also supports incoming data via acknowledgement packets, so you could also extend this to make things happen on remote nodes - controlling power, displaying results, whatever. This will require some changes to the central node to send these acknowledgements. It could even contain some custom "triggering rules" which apply to your situation. Turning lights on or off, presenting a summary on some displays, sending an alert if unusual conditions are detected. Again, the sky is the limit. The result could be a fully autonomous domotics network, consuming just a watt or so of power.
Note
- The above low-power modes all use the RF12 radio watchdog. It looks like this isn't reliable. See this discussion for details: http://talk.jeelabs.net/topic/108
- The latest "rooms" sketch contains improved low-power code, see http://code.jeelabs.org/rooms/