Monday, January 30, 2012

Mt Wilson Toll Road to Henninger Flats

The Mount Wilson toll road is a well-maintained dirt road that the LA county fire department uses to access their station at Henninger flats.  There's a gate off Pinecrest Road in Altadena - it's open to foot or bike traffic from sunrise to sunset every day. The segment from Altadena to Henninger flat is not the most beautiful hike in the San Gabriels (personally, I would rank it near the bottom of the list!), but there are some nice views and for me it's a convenient way to access more interesting territory higher up.  Also, this area was not touched at all by the Station Fire of 2009, so as of early 2012 there is still lots of mature chaparral along the slopes that the trail passes.

It can be a rough hike in the summer - there is very little shade and it's a steep climb in some places (the bridge is at about 1200' elevation and Henninger is at 2400').  But in the winter it's quite pleasant - some of the side canyons get almost no sunlight.

It's about a 5 mile round-trip hike from the Pinecrest gate in Altadena to Henninger. You can also start at the Eaton Canyon visitor center parking lot (off Altadena drive, just north of New York Dr), which is about a mile and a half down from the bridge over Eaton Canyon.  The trail between the visitor center parking lot up to the bridge (and under the bridge to a nice waterfall a little ways back) can be extremely crowded, especially on weekends.  Also, keep in mind that in late 2011, the county installed a gate at the Eaton Canyon visitor center parking lot which is open only from 7:30am to 5pm.  If you want to get an earlier start, you'd have to park on the street.

I headed up the trail towards Henninger on a warm January day.  Here's the bridge over Eaton Canyon:


Here's some Encelia farinosa.  It seems early for these to be blooming, but it's been a very warm and dry December and January.
Encelia farinosa

This is the view towards LA at the intersection of the Walnut Canyon trail and the Toll Road:
The Walnut Canyon trail drops down to Eaton Canyon from here.  A couple of months ago, the trail was widened and cleaned up a lot, so it's pretty easy going at the moment.  Expect more crowds along this trail than on the Toll Road, though.

There's a grove of mediterranean pine trees on the left side of this picture. Tom Chester says that many of these non-native trees were planted by the Sierra Club for shade along the trail.  These trees at least aren't invasive, unlike some other plants (see below!).  It's not the best trail for learning about the native plants of California.

Above this point, there are a few more switchbacks and you reach Henninger.  At Henninger Flats, there's a fire station, a museum, and a tree nursery.  They say that the purpose of the forest is for conservation, but since they mostly grow non-Californian planets I get the feeling that at one point they wanted to find out if they could commercially grow a bunch of trees in the San Gabriels.   In any case it makes a nice shady spot to hang out and look down on "civilization."  There are campsites available as well, though I've never slept up here.

Eaton canyon, Pasadena, and LA. viewed from Henninger flats.
It might be hard to tell from my mediocre photos, but it was clear enough that day that I could see Santa Barbara island as well as Catalina.  I'm always amazed by how loud the city is - on the hike up the toll road in some places you are suddenly shielded from the sounds of cars and it's wonderfully quiet.  Then you turn a corner and are blasted again.  That sound must be bathing all of us city-dwellers all the time but we have grown so used to it that we don't even notice.  Another reason to get far out into the wild as much as possible!

Henninger flats is high enough that you can start to see some high-elevation mountain wildlife.  There are lots of nuthatches up there, along with mountain chickadees, Stellar's jays, western bluebirds, and a few sparrow species.  There has been at least one white-headed woodpecker hanging around for several months, which is surprisingly low elevation for that species here in the San Gabriels.  There is also a beautiful leucistic red-tailed hawk (that means lacking pigment in its feathers, but not truly albino) that is often seen around Henninger flats.  Unfortunately I was not able to photograph either on this trip.
White-brested nuthatch at Henninger Flats
There are Western Grey Squirrels up there - they look so much more fluffy than the introduced Fox Squirrels or the California Ground squirrels that live down in the LA basin.


If you head up from here you can hike all the way to mount Wilson - though it's another 7 miles one way.   This time, I was on a schedule, so I had to head back down.

On the way down, I noticed an entire hillside covered with fountain grass; this stuff:
Fountain grass
It's a popular yard plant with pretty purple seed tufts.  It originally comes from the drier parts of Africa and Asia, so it does extremely well in California's mediterranean climate.  The problem is that those pretty seed tufts are so prolific that the plant has a nasty tendency to push out every other living thing, like in this spot.  Pretty much every little yellow tuft in this photo is a clump of fountain grass:
Hillside covered in fountain grass
I'm not sure how you would get rid of it here -  the only thing I can think of is 1. apply a massive airdrop of Roundup, 2. burn the entire hillside, and 3. repeat monthly.   The location is here.

This is an even nastier invasive plant, the Castor Bean:

Apparently the government encouraged people to grow it here in southern California during World War 2 because it can make a nice aircraft oil (and laxative!).   But the plant produces all kinds of weird chemicals, including ricin.  Harvesting the beans can give you permanent nerve damage.  So if you ever have to handle this plant, please wear gloves and a lot of clothing. 

This is an interesting report on the status of invasives in LA from the friends of the LA river.  It's from a decade ago, but it puts Castor bean as its highest priority to remove.  Maybe environmental groups could get Homeland Security grants to try to eliminate castor beans, so people like these dudes in Georgia won't be tempted.

I was able to bring my homemade GPS for a field test on this hike - more on that soon.

Sunday, January 15, 2012

Converting pressure to altitude

Now that I have my GPS and barometric pressure sensor up and running, I can try calibrating the pressure data into altitude.  Given the properties of air molecules and the strength of gravity, and how much air there is in the atmosphere, there's a relationship between altitude and pressure.  Fortunately, the physics has already been worked out and can be found on wikipedia.  They are related to each other by a power law:

Altitude = A - A (Pressure/P0)^B

where A = T0/L, B = (RL)/(gM), T0 = temperature at sea level, L = temperature lapse rate, R = universal gas constant, g = acceleration due to gravity, M = molar mass of dry air.

The other parameter is P0 -- the pressure at sea level, which is really just a measure of how much air there is stacked up in the location you happen to be in.  There is a standard value for sea level pressure, but it varies depending on the weather.  The best thing to do is solve for it if you already know your altitude at the start.

I  took 4 minutes of data at home, where I can simply look up my altitude on a topo map (it's 312 m).  Then after plugging into the equations from wikipedia, I can look at how it compares to the GPS altitude.  Aside from noise (or changes in the weather) these data points should be all be the same.

Look how nice and stable the barometric altitude (blue points) looks compared to the GPS altitude (green points)!  Good thing I went through the trouble of installing that BMP085.  The standard deviation of the blue data points is 0.6m and the standard deviation of the green points is 3.5 meters.  So if you integrate for 240 seconds you can get your elevation to 0.6m, but only 3.5m with the GPS.  Not bad!

Why is the GPS altitude so poor?  I found a blog post on the topic which nicely addresses why the reported altitude is wrong.  This is because in order to report altitude above sea level, you have to make a model of the shape of Earth, which is assumed to be an ellipsoid (a flattened sphere - squished in at the poles).  This model generally gives the shape of the Earth, but it is not perfect in in most places on Earth.  So I think this explains the fact that on average, the GPS tells me I'm around 320 meters above sea level, but in fact I know I am at 312m.  This blog post also goes on to say that many GPS units return the correction factor between the ellipsoid model and the true sea level (unfortunately the EM-406 I'm using does not!).

That explains the overall discrepancy between measured and true altitude, but it doesn't explain why the GPS altitude drifts all over the place.  The latitude and longitude data are incredibly accurate - I can see what part of my house I am in when I overlay the data points in Google Earth, which is much better than the 3.6m standard deviation in altitude I see over 4 minutes.    In any case, including a pressure sensor in this device has solved the problem.

Here's my python code that I used to read in my data and do the calibration and conversion of pressure to altitude.

 import numpy  
 import pylab  
 def get_sea_level_pressure(p,alt):  
   T0 = 288.15  
   g = 9.80665  
   L = 0.0065  
   R = 8.31447  
   M = 0.0289644  
   return p/(1-L*alt/T0)**((g*M)/(R*L))  
 def pressure_to_altitude(p,p0):  
   T0 = 288.15  
   g = 9.80665  
   L = 0.0065  
   R = 8.31447  
   M = 0.0289644  
   A = (T0/L)  
   B = (R*L)/(g*M)  
   return A - A * (p/p0)**B  
 # The altitude where I started the log file  
 calibration_altitude = 312.12 # meters  
 time,lat,lon,gps_alt,pressure,temperature = numpy.genfromtxt("GPSLOG00.TXT",comments="#",unpack=True)  
 p0 = get_sea_level_pressure(pressure[0],calibration_altitude)  
 pylab.plot(pressure_to_altitude(pressure,p0),'o',label='Barometer')  
 pylab.plot(gps_alt,'o',label='GPS')  
 pylab.ylabel('Altitude (m)')  
 pylab.xlabel('Seconds')  
 pylab.legend()  
 pylab.show()  

Friday, January 6, 2012

Arduino-based GPS and barometric altimeter

I've been wanting a GPS data logger to keep track of hikes, but I haven't seen any commercial devices that I like.  For one thing, GPS altitude is not very accurate so I wanted to incorporate a barometric altimeter (which can be very sensitive to short time scale changes in altitude, but has long term drifts due to weather).  I also want to get the raw data spit out by the GPS so I can play with it myself rather than having to go through third-party software.

Fortunately, there is a thriving open-source hardware community with all the answers!  Adafruit sells a kit for a nice Arduino-based GPS shield that logs to an SD card. It interfaces to a device called EM-406 which does all the GPS stuff within a single tiny package and communicates with a simple serial interface.  It's the kind of thing that lives inside most fancy cell phones these days.

I also bought the BMP085 pressure and temperature sensor already mounted on a board (so I wouldn't have to do any surface-mount soldering, with which I have no experience).  The sensor claims a 0.06 hPa pressure error, which could translate into altitude precision of half a meter!  That sounds impressive, we'll have to see how it really performs.

This was my first Arduino project, so there was a lot for me to learn.  The documentation with nice pictures and example code provided by ladyada made it extremely easy to assemble the GPS shield and get to the point of logging GPS data.

So far so good, but then it was time for my customization.  I first wired up the BMP085.  There's just 4 wires to connect and the adafruit GPS shield nicely feeds all the necessary connections through.

The communication with the sensor is done serially using the Arduino's analog 4 and 5 (which is great because it doesn't interfere with the all-digital defaults for the GPS) and the sensor is powered with the 3.3V pin and of course grounded.  Here's what it looks like when I'm done:



Despite my ugly soldering, it works just fine.  There is a nice software library for talking to the BMP085 from adafruit that makes it easy to read the data from this device at the same time as getting the GPS data.

Next I wanted to modify the code to parse the data I want from the GPS and dump it into the same data file as the pressure and temperature data.  This took a bit of hacking because the example code simply dumps the NMEA sentences (the raw data that the GPS module spits out) to the SD card.

I had to look up the format of the various NMEA data chunks.  The EM-406 user guide has everything I needed.  There are several different data modes for the EM-406, which I believe are standards used by different devices.   The GGA data mode contains GPS altitude, which I want to save so I can compare it to pressure altitude later.  GGA does not contain the GPS date, so I'll have to live without that.   I'm also parsing all the individual items from the raw data and logging them separately. Here's my code.

 #include <Wire.h>  
 #include <SD.h>  
 #include <BMP085.h>  
 #include <avr/sleep.h>  
 #include "GPSconfig.h"  
 #include <SoftwareSerial.h>  
 // power saving modes  
 #define SLEEPDELAY 0  
 #define TURNOFFGPS 0  
 #define LOG_RMC_FIXONLY 0  
 // Use pins 2 and 3 to talk to the GPS. 2 is the TX pin, 3 is the RX pin  
 SoftwareSerial gpsSerial = SoftwareSerial(2, 3);  
 // Baud rate of GPS unit  
 #define GPSRATE 4800  
 // Set the pins used   
 #define powerPin 4  
 #define led1Pin 5  
 #define led2Pin 6  
 #define chipSelect 10  
 #define BUFFSIZE 90  
 char buffer[BUFFSIZE];  
 uint8_t bufferidx = 0;  
 uint8_t fix = 0; // current fix data  
 uint8_t i,j,k;  
 char *parseptr;  
 float Temperature;  
 int32_t Pressure;  
 float lat,lon,temp;  
 char date[7];  
 char utc_time[10];  
 char alt[10];  
 File logfile;  
 // BMP085  
 BMP085 bmp;  
 // read a Hex value and return the decimal equivalent  
 uint8_t parseHex(char c) {  
  if (c < '0')  
   return 0;  
  if (c <= '9')  
   return c - '0';  
  if (c < 'A')  
   return 0;  
  if (c <= 'F')  
   return (c - 'A')+10;  
 }  
 // blink out an error code  
 void error(uint8_t errno) {  
  while(1) {  
   for (i=0; i<errno; i++) {  
    digitalWrite(led1Pin, HIGH);  
    digitalWrite(led2Pin, HIGH);  
    delay(100);  
    digitalWrite(led1Pin, LOW);  
    digitalWrite(led2Pin, LOW);  
    delay(100);  
   }  
   for (; i<10; i++) {  
    delay(200);  
   }  
  }  
 }  
 void readline(void) {  
  char c;  
  bufferidx = 0; // start at begninning  
  while (1) {  
   c=gpsSerial.read();  
   if (c == -1)  
    continue;  
   if (c == '\n')  
    continue;  
   if ((bufferidx == BUFFSIZE-1) || (c == '\r')) {  
    buffer[bufferidx] = 0;  
    return;  
   }  
   buffer[bufferidx++]= c;  
  }  
 }  
 void setup() {  
  WDTCSR |= (1 << WDCE) | (1 << WDE);  
  WDTCSR = 0;  
  Serial.begin(9600);  
  Serial.println("\r\nGPS and BMP085 logger");  
  pinMode(led1Pin, OUTPUT);  
  pinMode(led2Pin, OUTPUT);  
  pinMode(powerPin, OUTPUT);  
  digitalWrite(powerPin, LOW);  
  // make sure that the default chip select pin is set to  
  // output, even if you don't use it:  
  pinMode(10, OUTPUT);  
  // see if the card is present and can be initialized:  
  if (!SD.begin(chipSelect)) {  
   Serial.println("Card init. failed!");  
   error(1);  
  }  
  strcpy(buffer, "GPSLOG00.TXT");  
  for (i = 0; i < 100; i++) {  
   buffer[6] = '0' + i/10;  
   buffer[7] = '0' + i%10;  
   // create if does not exist, do not open existing, write, sync after write  
   if (! SD.exists(buffer)) {  
    break;  
   }  
  }  
  logfile = SD.open(buffer, FILE_WRITE);  
  if( ! logfile ) {  
   Serial.print("Couldnt create ");   
   Serial.println(buffer);  
   error(3);  
  }  
  Serial.print("Writing to ");   
  Serial.println(buffer);  
  logfile.println("# A GPS track data file");  
  logfile.println("# UTC time  lat  lon  gps_alt  pressure temperature");  
  logfile.println("# hhmmss.sss deg  deg  m     hPa    C");  
  logfile.println("#");  
  // connect to the GPS at the desired rate  
  gpsSerial.begin(GPSRATE);  
  Serial.println("Ready!");  
  gpsSerial.print(SERIAL_SET);  
  delay(250);  
  gpsSerial.print(GGA_ON);  
  delay(250);  
  //gpsSerial.print(GLL_OFF);  
  //delay(250);  
  //gpsSerial.print(GSA_OFF);  
  //delay(250);  
  //gpsSerial.print(GSV_OFF);  
  //delay(250);  
  gpsSerial.print(RMC_OFF);  
  delay(250);  
  //gpsSerial.print(VTG_OFF);   
  // gpsSerial.print(RMC_ON);  
  // delay(250);  
  gpsSerial.print(WAAS_ON);  
  bmp.begin();   
 }  
 void loop() {  
  char c;  
  uint8_t sum;  
  char *p;  
  // read a line of data  
  readline();  
  Serial.print("raw line: ");  
  Serial.println(buffer);  
  // get checksum  
  sum = parseHex(buffer[bufferidx-2]) * 16;  
  sum += parseHex(buffer[bufferidx-1]);  
  Serial.print("Checksum:");  
  Serial.println(sum);  
  // check checksum  
  for (i=1; i < (bufferidx-3); i++) {  
   sum ^= buffer[i];  
  }  
  if (sum != 0) {  
   //putstring_nl("Cxsum mismatch");  
   Serial.print('~');  
   bufferidx = 0;  
   return;  
  }  
  p = buffer;  
  // UTC time is item 2  
  p = strchr(p, ',')+1;  
  for (k=0;k<9;k++)  
   utc_time[k]=p[k];  
  Serial.print(" UTC time:");  
  Serial.println(utc_time);  
  // lat is item 3  
  p = strchr(p, ',')+1;  
  // get degrees and convert minutes to decimal degrees  
  lat = (p[0]-'0')*10.0 + (p[1]-'0') + ((p[2]-'0')*10.0 + (p[3]-'0') + (p[5]-'0')/10.0 + (p[6]-'0')/100.0 + (p[7]-'0')/1000.0 + (p[8]-'0')/10000.0)/60.0;  
  // N or S is item 4  
  p = strchr(p, ',')+1;  
  if (p[0]=='S')  
   lat *= -1.0;  
  Serial.print(" latitude:");  
  Serial.println(lat,7);  
  // lon is item 5  
  p = strchr(p, ',')+1;  
  // get degrees and convert minutes to decimal degrees  
  lon = (p[0]-'0')*100.0 + (p[1]-'0')*10.0 + (p[2]-'0') + ((p[3]-'0')*10.0 + (p[4]-'0') + (p[6]-'0')/10.0 + (p[7]-'0')/100.0 + (p[8]-'0')/1000.0 + (p[9]-'0')/10000.0)/60.0;  
  // E or W is item 6  
  p = strchr(p, ',')+1;  
  if (p[0]=='W')  
   lon *= -1.0;  
  Serial.print(" longitude:");  
  Serial.println(lon,7);  
  // fix indicator is item 7  
  p = strchr(p, ',')+1;    
  if (p[0]=='0') {  
   fix = 0;   
   digitalWrite(led1Pin, LOW);  
  }  
  else {  
   fix = 1;  
   digitalWrite(led1Pin, HIGH);  
  }  
  // alt is item 10  
  p = strchr(p, ',')+1;  
  p = strchr(p, ',')+1;  
  p = strchr(p, ',')+1;  
  j=0;  
  while (p[j] != ',') {  
   alt[j] = p[j];   
   j++;  
  }  
  Serial.print(" alt:");  
  Serial.println(alt);  
  bufferidx = 0;  
  // now start logging data  
  digitalWrite(led2Pin, HIGH);   // sets the digital pin as output  
  // write date  
  logfile.print(date);  
  logfile.print(" ");  
  // write UTC time  
  logfile.print(utc_time);  
  logfile.print(" ");  
  // write latitude  
  logfile.print(lat,7);  
  logfile.print(" ");  
  // write longitude  
  logfile.print(lon,7);  
  logfile.print(" ");  
  // write altitude  
  logfile.print(alt);  
  logfile.print(" ");  
  // get data from the BMP085 and log  
  Temperature = bmp.readTemperature();  
  Serial.print("Temperature = ");  
  Serial.print(Temperature);  
  Serial.println(" C");  
  Pressure = bmp.readPressure();    
  Serial.print("Pressure = ");  
  Serial.print(Pressure);  
  Serial.println(" Pa");   
  // write pressure  
  logfile.print(" ");  
  logfile.print(Pressure);  
  logfile.print(" ");  
  // write temperature  
  logfile.println(Temperature);     
  logfile.flush();   
  digitalWrite(led2Pin, LOW);  
  // turn off GPS module?  
  if (TURNOFFGPS) {  
   digitalWrite(powerPin, HIGH);  
  }  
  delay(SLEEPDELAY * 1000);  
  digitalWrite(powerPin, LOW);  
  return;  
 }  
http://codeformatter.blogspot.com/2009/06/about-code-formatter.html

Finally, I wanted to make a nice package for the device and set it up to run off a battery pack so I can use this device remotely.  On the adafruit website, they recommend putting it inside a fancy Otterbox, which seals very tightly and is extremely strong and completely waterproof, but it would complicate things for me since I want to measure the outside pressure.

So I settled on a nice $2 sandwich box for now.  It's solid and has plenty of room for all my extra stuff (maybe even a sandwich too!).  It won't be completely rain proof, but who hikes in the rain anyway?  Also since it doesn't hermetically seal I don't have to worry about mounting the pressure sensor on the outside of the box.  The temperature won't perfectly reflect the temperature of the outdoors since it's in the box with all the electronics, but for now I won't worry about it.  I also found a nice pull-chain on-off switch.

It's easy to use a different power source for the Arduino - it can handle a 9V battery.  I got a 2.1mm plug and connected it to a pull-chain switch pack that plugs into power jack on the Arduino.  I'm not sure how long an alkaline 9V battery will last running the BMP085 and the GPS and logging data every second - that will have to wait for tests.  If it seems too short, I can try slowing down the logging rate.

Et voila:

Now I'm looking forward to field testing!