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!