sun, 22-may-2011, 08:06
Arduino temperature display

Arduino temperature display

I’ve had an Arduino-based weather station since June 2009, but one problem with it has been that there hasn’t been any easy way to display the data in real time without going to the database (or the raw import files) to see what the latest observations were. I wrote a quick web page for this (home display), and this is convenient if I want to see what the temperatures are around the house when I’m not at home. But sitting in the living room, it’d be nice to know what the temperatures are just by looking.

The choices for displays aren’t all that great. Arduino can drive the classic seven segment LED displays (like old calculators, for those who remember what a calculator is), which are big and bright, but it requires a lot of pins, and even more power to light enough of them to display the amount of data I need (at least four temperatures, and possibly other sensor values like pressure or light). There are graphical LCD panels, which you control pixel by pixel but are expensive and aren’t all that large. And then there are text LCD displays that use the Hitachi HD44780 controller (or one based on it). These are more like old terminal screens where you can write to each character position on the screen. Common sizes are 16 characters across x 2 rows and 20 x 4 displays.

I chose a white on black, 16 x 2 display (SparkFun LCD-00709). It requires 5 volts to power the display and the LED backlight, and uses six of the digital pins on the Arduino board. The Arduino Uno (and earlier Duemilanove) have fourteen digital pins (two of which are the serial TX/RX pins), so one could hook up two of these displays to a single Arduino. The hookup for a single display, and the code I’m using follows. The Arduino Cookbook was invaluable for getting the everything working.

LCD / Arduino circuit diagram

The Arduino code reads four comma-delimited numbers from the serial port. They’re formatted as integer values representing the temperature in Fahrenheit multiplied by ten (50.2°F = 502). If the value is negative, the minus sign appears at the end of the number (-40.0°F = 400-). Here’s the Arduino code that displays the incoming data to the LCD:

#include <LiquidCrystal.h>

const int numRows = 2;
const int numCols = 16;
const int NUMBER_OF_FIELDS = 4; // t_west, t_back, t_up, t_down
int fieldIndex = 0;
int values[NUMBER_OF_FIELDS]; // temp * 10

LiquidCrystal lcd(12, 11, 5, 4, 3, 2);

void setup() {
  lcd.begin(numCols, numRows);
  lcd.print("SWINGLEYDEV.COM");
  Serial.begin(9600);
}

void displayVal(int val) {
  int ival;
  int dval;
  if (val >= 0) {
    ival = floor(val / 10.0);
    dval = val - (ival * 10);
  } else {
    ival = ceil(val / 10.0);
    dval = -1 * (val - (ival * 10));
  }
  if (ival >= 0) {
    lcd.print(" ");
  }
  lcd.print(ival);
  lcd.print(".");
  lcd.print(dval);
}

void loop() {
  if (Serial.available()) { // Read message
    delay(100);
    while (Serial.available()) {
      char ch = Serial.read();
      if (ch >= '0' && ch <= '9') {
        values[fieldIndex] = (values[fieldIndex] * 10) + (ch - '0');
      } else if (ch == ',') {
        if (fieldIndex < NUMBER_OF_FIELDS - 1) {
          fieldIndex++;
        }
      } else if (ch == '-') {
        values[fieldIndex] = values[fieldIndex] * -1;
      } else {
        lcd.clear();
        lcd.setCursor(0, 0);
        lcd.print("W "); // West outdoor sensor
        displayVal(values[0]);
        values[0] = 0;
        lcd.setCursor(9, 0);
        lcd.print("B "); // Back outdoor
        displayVal(values[1]);
        values[1] = 0;
        lcd.setCursor(0, 1);
        lcd.print("U "); // Upstairs
        displayVal(values[2]);
        values[2] = 0;
        lcd.setCursor(9, 1);
        lcd.print("D "); // Downstairs
        displayVal(values[3]);
        values[3] = 0;
        fieldIndex = 0;
      }
    }
  }
}

To make this work, I need a program on the computer the Arduino is plugged into that will read the weather data from the database and dump it to the serial port that the display Arduino is plugged into. Here’s that code:

#! /usr/bin/env python

import serial, time, shutil, os
import psycopg2

connection = psycopg2.connect(host='HOSTNAME', database='DB')
cursor = connection.cursor()
ser = serial.Serial('/dev/ttyACM0', timeout=1)

def make_arduino_str(value):
    integer = int(round(value * 10))
    if value < 0:
        return "{0}-".format(integer * -1)
    else:
        return "{0}".format(integer)

while 1:
    query = "SELECT west, back, down, up FROM arduino_view;"
    params = ()
    cursor.execute(query, params)
    if cursor.rowcount:
        rows = cursor.fetchall()
        for row in rows:
            (west, back, down, up) = row
        (west, back, down, up) = map(make_arduino_str,
                (west, back, down, up))
        message = "{0},{1},{2},{3}\n".format(west, back, up, down)
        ser.write(message)
    time.sleep(60)
ser.close()
cursor.close()
connection.close()

Each minute, it reads the temperatures from a database view that consolidates the latest readings from the weather sensors (arduino_view), formats them in the manner the Arduino display code expects, and sends them to the serial port for display.

It works really well, and looks good. I wish the display itself were larger, however. I’ll probably get a second LCD panel so I can display more data, but if someone would make a 20 x 4 display that was twice as large as the one I’ve got now, I’d buy it. The 16 x 2 is easy to read from the couch (five feet away), but isn’t readable from the kitchen.

tags: Arduino  weather  lcd 
sat, 21-may-2011, 07:29
Back cabin, high water

Back cabin, high water

I started measuring the depth of Goldstream Creek a little over a year ago this week. Each morning I measure down from a particular spot on the bridge over the Creek to the top of the water or ice and report this (plus daily high and low temperature, precipitation, snowfall and snow depth) to the Weather Service. The following plot (if nothing shows up, click the following links to view a PNG or PDF version) shows the depth of the Creek at our swimming hole on the top, and the daily high and low temperatures on the bottom. The dark cyan line on the top plot is the height of the bridge (about six feet below the entrance to our house), and the dark cyan line on the lower plot is the freezing point.

At it’s highest, the Creek was just over two feet from the bottom of the bridge, the slough flooded into the dog yard about two thirds of the way across the lowest point, and the Creek seemed dangerously close to topping the banks. The photo at the top shows the back cabin during the high water event.

This year’s breakup was similar to last year: ground and meltwater from the surrounding area started flooding on top of the ice and over the course of a couple weeks, it eroded the ice below until the water level rapidly dropped to more normal summertime depths. One interesting note is that we seem to get a large pulse of water (the rise starting around March 11th) before the snow has started melting, which would seem to indicate that the first pulse is coming from groundwater sources. We don’t start getting afternoon temperatures above freezing until the beginning of April, and this is when snowmelt starts bringing the level up even higher.

When the level begins to drop, it’s pretty dramatic as we go from bankfull conditions to almost nothing in a week. This year we’ve still got wide shelves of ice hanging on the banks six feet above the level of the water.

The plot also shows the brief ice storm in late November where we got a couple inches of rain that froze on the roads and brought the Creek up slightly.

In the past, we’ve had more dramatic breakups where the initial springtime pulse of ground and meltwater breaks up all the ice in a couple days and sends it past our house crashing and grinding, but even the more gradual melting pattern of the last two years is impressive in how quickly the Creek rises and falls.

sun, 15-may-2011, 11:25
Battery, Arduino, XBee

Battery, Arduino, XBee

Several years ago when I started messing around with Arduino and building my own weather station, I bought a few XBee radio chips with the idea of setting up some more remote sites without having to run wires out to the sensor stations. I spend several frustrating hours trying to get the radios to talk to each other, but couldn’t get them to work (I think this was because they were in API mode instead of AT mode). Then I got Building Wireless Sensor Networks and Arduino Cookbook and finally got everything working.

At the same time, the batteries in our UPS at work needed to be replaced, and rather than immediately recycling them, I took a couple home to see if they still had enough juice in them to work in a remote sensing capacity. They’re 12 volt lead-acid batteries (which may make them unsuitable for winter) that were rated for 5 Amp-hours when they were new. That should be plenty of power to drive an Arduino, XBee and a couple sensors. The XBee chips have some data pins on them, so I may be able to eliminate the Arduino from each sensor station, depending on the type of sensor I use.

The current setup, shown in the photo at the top, is designed to see how long a remote station can run on one of the batteries without any sensors and at springtime temperatures (typically between 20°F and 60°F at our house). The Arduino is reading the input voltage and sending it wirelessly to an XBee coordinator plugged into a SparkFun explorer board and connected to my small eeeBox computer.

Here’s the circuit diagram:

Circuit diagram

The analog input pins on the Arduino board are expecting voltages between 0 and 5 volts, so I’m using three resistors as a voltage divider to reduce the input voltage (nominally 12 volts, but potentially as high as 15V) to a range safe for the board. The sketch looks like this (almost identical to the sketch in the Arduino Cookbook except that the formula for resistorFactor is incorrect in the book):

const float referenceVolts = 5;

const float R1 = 2000; // A0 to V+
const float R2 = 1000; // A0 to GND

const float resistorFactor = 1023.0 * (R2/(R1 + R2));

const int batteryPin = 0; // A0

void setup() {
   Serial.begin(9600); // to XBee
}

void loop() {
   int val = analogRead(batteryPin);
   float volts =
     (val / resistorFactor) * referenceVolts * 10;
   Serial.println(volts);
   delay(1000);
}

Because the Arduino transmit pin (D1) is connected to the XBee receive pin (DIN) and the Arduino receive (D0) is connected to the XBee send (DOUT), anything the Arduino sends to the serial port is redirected to the XBee, which immediately transmits it wirelessly to the XBee receiver chip in the house.

At the moment, I’m only able to have the sender and receiver about 50 feet from each other before losing the signal, but I think that a pair of boards with whip antennas will work better than the chip antennas I’m currently using. I also set up my network such that the XBee Pro board (which has a supposed range of a mile) is the coordinator receiving messages, but I think it will work better as the sender. When the battery experiment is over, I’ll reverse the position of the XBee boards and see if I get better performance. I’d like to have a station out at the red cabin and potentially somewhere on the hillside, and for that to work the radios will need to be able to communicate over several hundred feet.

The circuit diagram was generated using circuit macros, which take circuit diagrams written in the m4 language and turn them into PIC files which are converted into LaTeX code using dpic. That sounds complicated (and it sort of is), but I much prefer describing the circuits in a text file than to trying to draw them using XCircuits or some other graphical tool. If you’re curious, you can download the code and a Makefile for generating the images.

Here’s what the data looks like so far:

Voltage over time

So far all I can see is a diurnal pattern associated with temperature: it was about 56°F at 6:30 PM last night when the data started, dropped to a minimum of 27°F at around 5:30 AM, and it’s been rising since, hitting 50°F at the end of the period shown on the plot.

sat, 05-mar-2011, 17:08

Nika, overflow

Nika, Goldstream Creek

The last couple days have seen a lot of overflow on Goldstream Creek, causing it to rise more than two feet. The water moved fast enough and it's been cold enough at night that it froze into a pretty good surface for ice skating. Many years ago we lived in a cabin at the edge of a pond near the railroad tracks and we bought ice skates so we skate on the pond. Turns out the number of days where the pond is frozen and not completely covered by snow is virtually zero, so we rarely got a chance to use them. But here, it seems that at least once or twice a year the overflow on the Creek or the DNR pond east of us will run over the snow and freeze into reasonably smooth ice.

I attempted to shoot a video while ice skating on the Creek today. It's not the greatest video, but it does give you some idea of what it looks like. After it freezes and before the overflow starts later in the winter, I spend a lot of time walking Nika and Piper down here. During breakup, the water rises to just below the bottom of the bridge, and then recedes to between four and five feet lower than where I'm skating by the middle of summer. The bridge I duck under is where I do my river stage measurements for the National Weather Service.

tue, 01-mar-2011, 18:47

Sunset, Miller Hill

Sunset, Miller Hill

People always ask if we’re the coldest spot in town. I can’t really answer that, but I can find out if we’re the coldest reporting weather station in the region.

Once again, we’ll use PostgreSQL window functions to investigate. The following query finds the station in zone 222 (the National Weather Service region that includes Fairbanks) reporting the coldest temperature every hour during the winter, counts up all the stations that “won,” and then ranks them. The outermost query gets the total number of hourly winners and uses this to calculate the percentage of hours that each station was the coldest reporting station.

Check it out:

SELECT station, count,
    round(count / sum(count) OVER (
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
    ) * 100, 1) AS percent
FROM (
    SELECT station, count(*) AS count
    FROM (
        SELECT station, dt_local, temp_f,
            rank() OVER (
                PARTITION BY dt_local ORDER BY temp_f
            )
        FROM (
            SELECT location || ' (' || station_id || ')' AS station,
                date_trunc('HOUR', dt_local) AS dt_local, temp_f
            FROM observations
                INNER JOIN stations USING (station_id)
            WHERE zone_id = 222
                AND dt_local between '2010-10-01' and '2011-03-31'
        ) AS foo
    ) AS bar WHERE bar.rank = 1 GROUP BY station ORDER BY count desc
) AS foobar;

And the results:

                station                 | count | percent
----------------------------------------+-------+---------
 Goldstream Creek (DW1454)              |  2156 |    51.0
 Chena Hot Springs (CNRA2)              |   484 |    11.5
 Eielson Air Force Base (PAEI)          |   463 |    11.0
 Parks Highway, MP 325.4 (NHPA2)        |   282 |     6.7
 Small Arms Range (SRGA2)               |   173 |     4.1
 Ballaine Road (AS115)                  |   153 |     3.6
 Fairbanks Airport (PAFA)               |   125 |     3.0
 Fort Wainwright (PAFB)                 |   107 |     2.5
 Ester Dome (FBSA2)                     |   103 |     2.4
 Eagle Ridge Road (C6333)               |    81 |     1.9
 Keystone Ridge (C5281)                 |    33 |     0.8
 Skyflight Ave (D6992)                  |    21 |     0.5
 14 Mile Chena Hot Springs Road (AP823) |    21 |     0.5
 College Observatory (FAOA2)            |    11 |     0.3
 Geophysical Institute (CRC)            |    10 |     0.2
 DGGS College Road (C6400)              |     1 |     0.0

Answer: Yep. We’re the coldest.

Update: Thinking about this a little bit more, the above analysis is biased against stations that don't report every hour. Another way to look at this is to calculate the hourly average temperature, subtract this from the data for each station during that hour, and then average those results for the whole winter. The query is made more complex because several stations report temperatures more than once an hour. If we simply averaged all these observations together with the stations that only reported once, these stations would bias the resulting hourly average. So we average each station's hourly data, then use that to calculate the zone average for the hour. Here's the query, and the results:

SELECT station, 
    round(avg(diff), 1) AS avg_diff 
FROM (
    SELECT station,
        dt_local, 
        temp_f - avg(temp_f) 
            OVER (
                PARTITION BY dt_local 
                ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
            ) AS diff 
    FROM (
        SELECT location || ' (' || station_id || ')' AS station, 
            date_trunc('HOUR', dt_local) AS dt_local, 
            avg(temp_f) AS temp_f 
        FROM observations 
            INNER JOIN stations USING (station_id) 
        WHERE zone_id = 222 AND 
            dt_local between '2010-10-01' and '2011-03-31' 
        GROUP BY station, date_trunc('HOUR', dt_local)
    ) AS foo
) AS bar 
GROUP BY station 
ORDER BY avg_diff;
                station                 | avg_diff
----------------------------------------+----------
 Goldstream Creek (DW1454)              |     -6.8
 Eielson Air Force Base (PAEI)          |     -3.8
 Fort Wainwright (PAFB)                 |     -3.1
 Fairbanks Airport (PAFA)               |     -2.9
 Small Arms Range (SRGA2)               |     -2.8
 Chena Hot Springs (CNRA2)              |     -2.3
 DGGS College Road (C6400)              |     -0.7
 Ballaine Road (AS115)                  |     -0.6
 College Observatory (FAOA2)            |      1.0
 North Bias Drive (RSQA2)               |      1.3
 14 Mile Chena Hot Springs Road (AP823) |      3.1
 Skyflight Ave (D6992)                  |      3.3
 Geophysical Institute (CRC)            |      3.5
 Eagle Ridge Road (C6333)               |      3.8
 Parks Highway, MP 325.4 (NHPA2)        |      4.5
 Keystone Ridge (C5281)                 |      5.1
 Ester Dome (FBSA2)                     |      5.1
 Birch Hill Recreation Area (BHS)       |      6.8

<< 0 1 2 3 4 5 6 7 8 9 10 11 12 13 >>
Meta Photolog Archives