Finally for everyone, fully functional GPS logger

(Update: new video is uploaded with titles and captions.)


So finally for me and everyone else interested, I've made it! A fully functional GPS logger. It features a menu with a few items. It records GPS info (lat/long/alt/date/time/speed) to an EEPROM onboard my Phi-1 shield. With a 32KB EEPROM, it can record 200 entries. You choose how often to record under parameters in the menu. You can also choose to record between say entry 100 and 200 and it will record and quits to menu when done. Then you may connect the arduino to a computer and export everything in a verbose mode (human-readable) or in spreadsheet mode (for excel if you want to analyze how often you go over speed limit).

I will have to give my project an A, compared with a recent one that is in a much less organized shape. ;D

The code, connection, more pictures are all on my blog. Comments are welcome!

Side note: I proved my shield can do LCD, buttons, speaker, LED, EEPROM, RTC, RJ11 connection, and GPS! Check out this picture to see how everything can fit under the hood (two chips hanging below the shield, LCD above it and GPS sandwiched between the shield and the LCD)!

Short video:

Top two functions are original from Mikal Hart. I wrote the two that compact the GPS info into 16 bytes and save to EEPROM

void printFloat(double number, int digits)
{
  // Handle negative numbers
  if (number < 0.0)
  {
     Serial.print('-');
     number = -number;
  }

  // Round correctly so that print(1.999, 2) prints as "2.00"
  double rounding = 0.5;
  for (uint8_t i=0; i<digits; ++i)
    rounding /= 10.0;
  
  number += rounding;

  // Extract the integer part of the number and print it
  unsigned long int_part = (unsigned long)number;
  double remainder = number - (double)int_part;
  Serial.print(int_part);

  // Print the decimal point, but only if there are digits beyond
  if (digits > 0)
    Serial.print("."); 

  // Extract digits from the remainder one at a time
  while (digits-- > 0)
  {
    remainder *= 10.0;
    int toPrint = int(remainder);
    Serial.print(toPrint);
    remainder -= toPrint; 
  } 
}

bool feedgps()
{
  while (nss.available())
  {
    if (gps.encode(nss.read()))
      return true;
  }
  return false;
}
// The code above this line was contributed by Mikal Hart (http://arduiniana.org)
void GPS_to_EEPROM(unsigned long *pointer)
/*
The function assumes that the TinyGPS object gps is already initialized and ready to send data.
It also assumes that the caller feeds the GPS and checks the pointer so the pointed address will not exceed the address space of the EEPROM, or cross page boundaries while writing.
*/
{
  double spdf;
  unsigned long spd;
  long lat, lon, alt;
  unsigned long age, dat, tim;
  unsigned long buf[4];
  
  gps.get_position(&lat, &lon, &age);
  gps.get_datetime(&dat, &tim, &age);
  alt=gps.altitude();
  spdf=gps.f_speed_mph();

  spd=spdf*100;
  buf[0]=lat;
  buf[1]=lon;
  buf[2]=((alt/10)<<16)|((dat/10000)<<11)|(((dat%10000)/100)<<7)|(dat%100); // Altitude only takes 16 bit. It is in the unit of 0.1m instead of 1cm, which is not necessary in accuracy. date(when expressed in binary 5bit date-4bit month-7bit year, takes no more than 16 bit. Combine them together.
  buf[3]=(spd<<17)|((tim/1000000)*3600+((tim%1000000)/10000)*60+(tim%10000)/100); // Speed, expressed in 1/100 mph, takes less than 15 bits. Compact hhmmsscc into seconds since 00:00 and lose the 1/100 seconds. This is 17 bit long.
  i2c_eeprom_write_page(0x50, (unsigned int) (*pointer), (byte*) buf, 16); // Store 16 bytes of data at location of the pointer.
  (*pointer)=(*pointer)+16; // Increment pointer.
  delay(5); // Make sure the data is written to the EEPROM.
}

boolean EEPROM_to_GPS(long *lat, long *lon, long *alt, unsigned long *tim, unsigned long *dat, double *spdf, unsigned long *pointer)
/*
This function reads one EEPROM entry, if it's non zero, parse it into long integer forms (double precision for speed), and returns true.
If the EEPROM entry is empty, return false.
To stay isolated from the main program, this function doesn't check if the pointer will be beyond the EEPROM size, which is left to the caller to do.
*/
{
  unsigned long buf[4];
  i2c_eeprom_read_buffer (0x50, (unsigned int) (*pointer), (byte*) buf, 16);
  if ((buf[0]==0)&&(buf[1]==0)&&(buf[2]==0)&&(buf[3]==0)) return false;
  *lat=(long)buf[0];
  *lon=(long)buf[1];
  *dat=buf[2]&0xFFFF;
  *dat=(*dat>>11)*10000+((*dat>>7)&15)*100+(*dat&127); //Process data to turn into 12/27/10 form
  *alt=10*((long)buf[2]>>16);
  *tim=(buf[3]&0x1FFFF);
  *tim=(*tim/3600)*1000000+((*tim%3600)/60)*10000+(*tim%60)*100; //Process time to turn into 12:59:0000 form
  *spdf=((double)((buf[3])>>17))/100.0;
  (*pointer)=(*pointer)+16; // Increment pointer by 16 bytes.
  return true;
}

Menus and functions:

void msg_lcd(char* msg_line)
{
  char msg_buffer[17];
  strcpy_P(msg_buffer,msg_line); 
  lcd.print(msg_buffer);
}

void render_menu(int me)
{
  char menu_buffer[20];
  lcd.clear();
  msg_lcd(msg_07);
  lcd.setCursor(0,1);
  strcpy_P(menu_buffer,(char*)pgm_read_word(&(menu_item[me]))); 
  lcd.print(menu_buffer);
}

void do_menu()
{
  int temp1;
  temp1=hmi_with_update_2(menu_pointer,0,n_menu_items-1,1,16,1,0,render_menu); // Sticky menu item.
  menu_pointer=(temp1==-1)?menu_pointer:temp1; // In case escape was triggered, value doesn't change.
  switch (temp1)
  {
    case menu_PC:
    _send_to_PC();
    break;

    case menu_erase:
    _erase();
    break;

    case menu_record:
    _record();
    break;

    case menu_display:
    _display();
    break;

    case menu_para:
    _parameters();
    break;

  }
}

void _record_display()
{
  double spdf;
  unsigned long spd;
  long lat, lon, alt;
  unsigned long age, dat, tim;
  unsigned long buf[4];
  char msg[17];

  bool newdata = false;
  unsigned long start = millis();
  // Every few seconds we print an update
  pointer=lower_limit; // Load the lower limit.
  while(1)
  {
    while (millis() - start < period*1000)
    {
      if (feedgps())
        newdata = true;
        
    }
    start=millis();
    if (newdata) // Update GPS coordinates on LCD
    {
      gps.get_position(&lat, &lon, &age);
      lcd.clear();
      sprintf(msg,"Lat:%ld",lat);
      lcd.print(msg);
      lcd.setCursor(0,1);
      sprintf(msg,"Long:%ld",lon);
      lcd.print(msg);
      if (recording)
      {
        if (pointer<upper_limit)
        {
          GPS_to_EEPROM(&pointer);
          lcd.setCursor(15,0);
          lcd.write(1);
        }
        else
        {
          lcd.clear();
          lcd.print("Limit reached");
          wait_on_escape(2000);
          return;
        }
      }
    }
  }
}

void _send_to_PC()
{
  double spdf;
  unsigned long spd;
  long lat, lon, alt;
  unsigned long age, dat, tim;
  unsigned long buf[4];
  pointer=0;
  if (!verbose) Serial.println("Lat(10^-5 deg)\tLong(10^-5 deg)\tDate(ddmmyy)\tTime(hhmmsscc)\tAlt(cm)\tSpeed(mph)");
  while (EEPROM_to_GPS(&lat, &lon, &alt, &tim, &dat, &spdf, &pointer)&&(pointer<=EEPROM_size-16))
  {
    switch (verbose)
    {
      case false:
      Serial.print(lat); Serial.print("\t"); Serial.print(lon); Serial.print("\t");
      Serial.print(dat); Serial.print("\t"); Serial.print(tim); Serial.print("\t");
      Serial.print(alt); Serial.print("\t"); printFloat(spdf); Serial.println("");
      break;
      
      case true:
      Serial.print("Lat/Long(10^-5 deg): "); Serial.print(lat); Serial.print(", "); Serial.print(lon); Serial.println("");
      Serial.print("Date(ddmmyy): "); Serial.print(dat); Serial.print(" Time(hhmmsscc): "); Serial.print(tim); Serial.println("");
      Serial.print("Alt(cm): "); Serial.print(alt); Serial.print(" Speed(mph): ");  printFloat(spdf); Serial.println(""); Serial.println("");
      break;
      
      default:
      break;
    }
  }
  pointer=0;
}

void _erase()
{
  int temp1, temp2;
  unsigned long buf[4]={0,0,0,0};
  lcd.clear();
  lcd.print("Erase all data?");
  temp1=0;
  temp2=hmi_with_update_3(&temp1, 0, 1, 1, 1, 1, 3, render_YN_in_place); // Asks whether outputs to PC in verbose mode.
  temp1=(temp2==-1)?0:temp1; // In case escape was triggered, value doesn't change.
  if (temp1)
    {
    lcd.clear();
    lcd.print("Erasing...");
    lcd.setCursor(0,1);
    lcd.print("Please wait");
    for (unsigned long addr=0;addr<EEPROM_size;addr+=16)
    {
      i2c_eeprom_write_page( 0x50, addr, (byte*) buf, 16);
      delay(5);
    }
  }
}

void _record()
{
  recording=true;
  _record_display();
}

void _display()
{
  recording=false;
  _record_display();
}

void _parameters()
{
  int temp1, temp2;

  lcd.clear(); // Input period
  lcd.print("Record Period:");
  lcd.setCursor(9,1);
  lcd.print("Seconds");
  temp1=period;
  temp2=hmi_with_update_3(&temp1, 1, 32000, 1, 4, 1, 4, render_number_in_place); // Asks for period between recordings.
  period=(temp2==-1)?period:temp1; // In case escape was triggered, value doesn't change.

  delay(100); // Input verbose mode
  lcd.clear();
  lcd.print("Verbose mode:");
  temp1=verbose;
  temp2=hmi_with_update_3(&temp1, 0, 1, 1, 6, 1, 3, render_YN_in_place); // Asks whether outputs to PC in verbose mode.
  verbose=(temp2==-1)?period:temp1; // In case escape was triggered, value doesn't change.

  delay(100); // Input lower recording limit
  lcd.clear();
  lcd.print("Record from:");
  lcd.setCursor(0,1);
  lcd.print("Entry#");
  temp1=(lower_limit/16);
  temp2=hmi_with_update_3(&temp1, 0, (EEPROM_size/16), 1, 7, 1, 5, render_number_in_place); // Asks the lower limit of the recording.
  lower_limit=(temp2==-1)?lower_limit:temp1*16; // In case escape was triggered, value doesn't change.

  delay(100); // Input upper recording limit
  lcd.clear();
  lcd.print("Record till:");
  lcd.setCursor(0,1);
  lcd.print("Entry#");
  temp1=(int)(upper_limit/16UL);
  temp2=hmi_with_update_3(&temp1, 0, (EEPROM_size/16), 1, 7, 1, 5, render_number_in_place); // Asks the upper limit of the recording.
  upper_limit=(temp2==-1)?upper_limit:((unsigned long)temp1)*16UL; // In case escape was triggered, value doesn't change.
}

Here is the limit of one post. You can find the rest of the code on my blog.

Today I took my GPS logger out for a road trip. Thanks to the LCD, menu, and a car adapter (12VDC), I didn't have to bring my laptop along, which may just get wet in the rain.

Here it is sitting in my car:

My pretty boring shopping trip:
Starting from top left, the parking lot, ending outside my apartment complex.

I will certainly take the logger out for more trips next week!

Awesome!

Are you powering the arduino directly from the lighter socket?

Yes, I discovered a car adapter a few days ago in my box of wires and tested it out on my car, 12VDC. I really don't like the barrel style power connector since there's so many different sizes and just a little difference will make the connection flaky. This one is a bit loose but it managed to make a good enough connection at a certain orientation. The acrylic stand with rubber feet also helped against sliding while I drove.