Sunday, November 6, 2011

Project: Wireless solar-powered garden moisture sensor

We recently built a box garden in our back yard, and we integrated it into the yard's existing irrigation system.  Unfortunately tuning the amount of water it should receive was somewhat clumsy, and it occurred to me that a sensor monitoring the moisture of the soil would be helpful in ensuring our garden gets the right amount of water.

  • Wireless, solar-powered
  • Measures soil moisture (dielectric constant method)
  • Measures soil temperature
  • Measures solar panel voltage
Hardware cost: about $125 (+$25-30 for the hardware to program the XBee)

Note that this post does not discuss the "base station" side of the wireless system.  I'll follow up with a separate post about that.


I chose XBee because I have an established XBee network, and I'm familiar with the devices and they seem to be power-efficient.  The XBee also has four analog-to-digital converters built in, so I also decided to try building this without a microcontroller at all, to further save power.  This meant all of my sensors had to be analog.

For power, if I have the XBee sleep for 30 seconds at a time, it should be able to run at less than 3-5mA on average (1.6mA for the device while it's sleeping, and less than 40mA while it's awake).  For the battery, we should have enough energy to last several days without sunlight, and when it's sunny we should be able to fully recharge in a day or so.  I ended up with a 1300mAh LiPo battery (10 days at 5mA) and a 2W solar panel (333mA at 6V).

For the soil moisture sensor, I wanted something analog, maintenance-free and something I could bury.  There are many sensors (especially DIY sensors) of the "stick two nails into the soil" variety.  These are unreliable and corrode over time.  Vegetronix makes a VG400 and VH400 analog sensor that measures the dielectric constant of the soil.  I went with the VH400 sensor because I figured I wanted better precision at the expense of a little bit of power.

Parts list (proto-board friendly approach):
  • Vegetronix VH400 soil moisture sensor ($34.95), analog 0-3V output (also consider the VG400)
  • Insulated thermistor (Adafruit $4.00), 10k ohms 
  • XBee 2mW Wire Antenna - Series 1 or 2 (ZB) (SparkFun $25.95)
  • XBee break-out, to mate the XBee with a breadboard or proto board
    • Breakout Board for XBee Module (SparkFun $2.95)
    • 2x 2mm 10pin XBee Socket (SparkFun $2.00)
  • XBee computer interface for programming, either XBee Explorer USB (SparkFun $24.95), or XBee FTDI break-out (Adafruit kit $10.00) and FTDI cable (Adafruit $20.00)
  • USB LiPoly Charger - Single Cell (SparkFun $14.95, Adafruit $20.00)
    • Note: The SparkFun charger comes set to 5v.  The selection jumper (a solder blob) must be removed and jumpered to 3.3v for the Xbee.
    • Note: The two chargers linked above are different.  The Adafruit charger would probably require the "XBee Explorer Regulated" (see links in the substitutions paragraph below).
  • LiPo battery (SparkFun 2000mAh $16.95, Adafruit 1300mAh $12.00)
  • Solar Panel, 6V 2W, weather-resistant (Adafruit $25.00)
    • This could probably be lower power, but this was the only one I could find that was also weather and UV resistant.
    • Pay attention to the connector on the charger above.  You may also need to grab a mini USB cable, for instance, and solder it to the solar panel.
  • Break Away Headers - Straight (SparkFun $2.50) -- I always keep some of these around because I use them a lot when mating break-out boards to breadboards.
  • Breadboard or proto board
    • I started with:  Breadboard Clear Self-Adhesive (SparkFun $5.95)
    • And then transferred everything to: ProtoBoard - Rectangle 3" (SparkFun $4.50)
      • Consider also getting the female complement to the Break Away Headers above to avoid making some of the connections permanent.
  • A weather-proof case, e.g.:
    • Small waterproof OtterBox (Adafruit $10.00)
    • SparkFun Project Case - Black (SparkFun $9.95)
  • Resistors for three voltage dividers (I used a mix of what I had handy).  The XBee only measures up to 1.2v.
    • 3.0v -> 1.2v for measuring the moisture sensor (I used a 6.8k and two 5.6k in series)
    • 3.3v + 10k ohm thermistor -> 1.2v (I used 3.3k)
    • 6v -> 1.2v for measuring solar panel voltage (I used a 3.3k and two 6.8k in series)
Substitutions are possible.  The battery charger just needs to be able to accept the voltage provided by the solar panel, and must be compatible with whatever battery you're using.  The charger should permit simultaneous powering of your circuit while charging (or not), and should provide a regulated 3.3v output.  If you can't get a regulated output, substitute something like the "XBee Explorer Regulated (SparkFun $9.95)" for the Xbee break-out board and be aware that the pins on the XBee Regulated are different from SparkFun's break-out board.

Here's a basic breadboard arrangement of the above:

The LiPo charger is the red component at the top, and has the "SYS" or "Project" lines connected to the breadboard as shown (inside rail is positive, outside is ground).  The third wire on the left side of the charger connects to the solar panel as well, though you could conceivably connect it to the battery's positive terminal if you'd rather see battery voltage instead.  The solar panel then connects to the LiPo charger (not depicted).

The two-port screw terminal connects to the thermistor.  Orientation doesn't matter.  The four-port screw terminal connects to the VH400 sensor.  As depicted here, port 1 is the signal (black wire of the VH400), port 3 is the supply voltage (red) and port 4 is ground (bare).

I strongly suggest modifying this approach to power the VH400 from the XBee's ON pin, so that it's only powered when the XBee is awake.  The XBee is not guaranteed to supply a full 3.3v from this pin, so you may also need to use a transistor to power the VH400 from Vcc.  The layout above is what I ended up doing, though.


Since the node itself has no microcontroller, I only needed to be concerned about (a) configuration of the XBee, and (b) the software on the server side to interpret and record the data.

XBee Configuration
I'm using a Series 2 (ZB) XBee module, so the instructions below might vary if you're using something different.  I'm not taking advantage of any of the Zigbee features of the Series 2, so the Series 1 XBees should work fine (and they're cheaper).  Use the XBee Explorer USB or FTDI adapter to connect the XBee to your computer, and use XCTU to flash the 'router' firmware onto the XBee.  Search the web for tutorials on obtaining and using XCTU with XBee.  The 'router' firmware will give you a flexible set of features.  The 'end device' firmware will automatically put the XBee in a cyclic sleep mode, which is a pain to work with.  With the right firmware in place, you can use XCTU or a serial terminal program to configure the XBee.  You may want to start with "ATFR" or "ATRE" to reset the XBee's configuration to factory defaults.  After each set of commands, you'll probably also want to do an "ATWR" to write your settings to non-volatile memory.

First, let's identify the module:


ATDH and ATDL will give you the upper and lower 32-bits of the node's 64-bit address.  Record these for later.  Now let's configure the ADIO pins to be analog inputs (2).

ATD0 2
ATD1 2
ATD2 2

And let's have the XBee periodically report them (say, every 5s (0x1388 ms)).  We leave the destination address defaulted to 0, which will cause the samples to be sent to the XBee coordinator (our base station).

ATIR 1388

Since my network is encrypted, I also need to set an encryption key and enable encryption before this will work.  Skip setting KY and EE unless you need to.

ATKY 0011223344556677

A few seconds later, the XBee should be joined to the network and transmitting samples to your coordinator.  Verify that you're receiving these before continuing.  Once the XBee seems to be reliably sending its analog samples, there's a few more changes we can make to reduce its power consumption.

First, we can reduce the transceiver power.  By default, the PL setting is at full transmit power (4), and the PM receive boost setting is enabled (1).  Try reducing these as much as you can, while still allowing you to receive messages.  You may need to do this once your device is installed in its final home.


Finally, we can put it to sleep between samples.  I have mine set to wake every 30s, but 1 to 5 minutes might be acceptable too, and will extend the battery life.  The time spent asleep is the product of the SP value (in 10ms increments up to 2800ms (0xaf0)) and SN (a simple multiplier).  We'll also tell it to stay awake for only 100ms (0x64).


After sending the ATSM 4 command, the XBee will start sleeping as instructed.  Your ability to make further configuration changes will be hampered.  If you're using XCTU, you'll get an error trying to send anything while the XBee is sleeping.  While XCTU is giving you this error, you can wake the XBee up by grounding the RST pin, at which point XCTU should discover it again.  Alternatively, you can make your changes remotely via another XBee (using the "remote AT" API), which will buffer commands until the XBee wakes up and checks in with its parent node.  You may also have success using ATSM 5, and using the wake pin feature to keep it awake while you're messing with it.  I haven't tried that.

You'll want to use the same (or greater) SN and SP values for the parent XBee node (either the coordinator, or an upstream router).  Otherwise, the parent node will forget about the child since it will be asleep so much, causing you to lose commands that you might want to send to it, and requiring the child to do a more expensive join to the network.

Base Station Software
On the receiving end, you'll need another XBee and a computer interface (similar to what you needed to program the XBee above) acting as a controller.  With the XBee accessible by serial port, and in API mode, you can adapt this Python script to receive samples.


The graphs below should have live data, constructed with RRDtool.  (Essentially, I just used the sample Python script above to execute an 'rrdtool update' command that adds all three measurements to an RRD file that I can then graph.)  If there are gaps in the data (red lines), note that I'm still tuning the solar panel placement.

Update 2012-05-07: After about a year in operation, I've stopped getting readings.  I suspect the battery has died.  Until I get it working the graphs below will be blank or just give you outside temperatures (measured separately). 

  • The VH400 was an unnecessary power expense.  I think I could have gotten by with the VG400 and saved a lot of power in the process (~7mA versus 0.6mA).
  • The VH400 could have been driven from the XBee's ON pin, so that it was only consuming current when the XBee was awake.  This is undoubtedly the biggest power drain.
  • Cold solder joints will haunt you when deploying something like this someplace with lots of temperature variation.
  • Solar panel placement is important.  Despite getting a lot of California sun during the day, I ended up getting about half a day of charge out of my solar panel each day.  I'm still tuning this, given that my garden is growing up all around and finding a place without shadows is difficult.
  • Some way to monitor battery voltage would have been nice.  There's one more ADIO port on the XBee that could be used for this, but the LiPo charger I used didn't have an easy way to access the battery terminal.  The XBee is capable of reporting its own voltage, but this would have been regulated at 3.3v by the LiPo charger.

Tuesday, February 13, 2007

Firefox date handling in Page Info

Today I was researching an HTTP caching problem and I discovered what I thought was a bug. When viewing the Page Info for an HTTP resource in Firefox, there is a "Modified" and "Expires" date that should reflect the modification date of the resource and when the resource should expire from the browser's cache. Here is what I was seeing:

Modified:  Tuesday, February 13, 2007 6:38:58 PM
Expires:   Tuesday, February 13, 2007 6:38:03 PM

For the life of me, I could not understand why the modification date was after the expiration date. A helpful user wrote in with his own numbers that gave the expiration date as being a minute-and-a-half in the future of the modification date. It was supposed to expire exactly 20 seconds after it was modified. What was going on here?

Take a look at the response headers. This request was made when the local clock on my PC was exactly 00:38:00 GMT:

Date:          Wed, 14 Feb 2007 00:39:14 GMT
Last-Modified: Wed, 14 Feb 2007 00:38:58 GMT
Expires:       Wed, 14 Feb 2007 00:39:18 GMT
Cache-Control: max-age=3

See what's going on yet?

The clocks on servers and client systems can't be guaranteed to be synchronized. So when a server sends a Last-Modified or Expires date, the client can't really be sure when those events occur. HTTP solves this by including a Date header, which should match the server's clock at the time the response was sent. Browsers can then compare this clock to the client PC's clock to figure out when the server really intended the resource to expire.

Firefox is really doing the only reasonable thing it can do here. Cache expiration is something only Firefox can do. Since Firefox needs to know when, relative to its local clock, a resource should expire, it needs to compare the server's intended expiration date with its own clock. This means it has to adjust the resource's expiration date to compensate for the difference in clocks. The modification date, on the other hand, is something that's really only meaningful to the server. When Firefox decides that it needs to make a request to the server, it has to provide the Last-Modified date exactly as the server did, so that the server knows when it's appropriate to send back an updated resource, or just tell the browser to use what it already has cached.

The result? The Modified and Expires dates on the Page Info tab are showing times from the perspective of two different clocks. They cannot be compared to one another!

More reasons people don't want to deal with HTTP caching!