Arduino GPS logger with LCD code

Hello all.

I just wanted to know if there is anybody out there that can help me a bit, with polishing my code.

I started a project after a lot of research to create a GPS logger with an LCD screen. I want to use this device to monitor the speed in my classic car. At the same time it would be nice to plot a track. After a lot of work I have finally got my Arduino gps logger working on my own. However the code might not be the most optimal. And with turning on and of of the arduino it starts logging in the middle of the previous (broken) line in the file on the SD card.

The hardware:

  1. Arduino Uno
  2. GPS logger board on basis of the U-Blox neo 6m with an SD card reader from ELECROW.
  3. Blue LCD screen 16x2 with I2C backpack
  4. perforated prototyping board to attach everything to.

Goals:
Display speed
Display heading
Display logging status
Display voltage of a carsensor

Log beginning of track
log latitude
Log longitude
log course
log speed
log date
log time

here is my code:

#include <SoftwareSerial.h>
#include <TinyGPS.h>
#include <LiquidCrystal_I2C.h> // more info: http://arduino-info.wikispaces.com/LCD-Blue-I2C
#include <SD.h>
#include <SPI.h>

// set up variables using the SD utility library functions:
Sd2Card card;

TinyGPS gps;
SoftwareSerial ss(3, 2); //RX, TX
LiquidCrystal_I2C lcd(0x27, 2, 1, 0, 4, 5, 6, 7, 3, POSITIVE);  // Set the LCD I2C address
File mySensorData; //Data object you will write your sesnor data to
const int chipSelect = 10;

int temp = 80;
int stat;
unsigned long pos;
String newtrack = "1";
String filename = "NMEA.txt";
static void smartdelay(unsigned long ms);
static void print_float(float val, float invalid, int len, int prec);
static void print_int(unsigned long val, unsigned long invalid, int len);
static void print_date(TinyGPS &gps);
static void print_str(const char *str, int len);
static void log_float(float val, float invalid, int len, int prec);

void setup()
{
  pinMode(chipSelect, OUTPUT); //Must declare 10 an output and reserve it to keep SD card happy

if (!card.init(SPI_HALF_SPEED, chipSelect)) {
    stat = 0;
  } else {
     stat = 1;
  }
  
  SD.begin(chipSelect); //Initialize the SD card reader   
  
  Serial.begin(115200);
  lcd.begin(20,4);         // initialize the lcd for 20 chars 4 lines and turn on backlight
  lcd.setCursor(0,0);
  lcd.write("GPS SYSTEM START");
  
 smartdelay(1000);
  lcd.clear();
  
  lcd.setCursor(4,0);
  lcd.write("Kmh");

  lcd.setCursor(4,1);
  lcd.write("DegC");

  lcd.setCursor(8,0);
  lcd.write("H:");

  lcd.setCursor(10,1);
  lcd.write("LoG:");

  ss.begin(9600);
}

void loop()
{
  float flat, flon;
  unsigned long age, date, time, chars = 0;  
  gps.f_get_position(&flat, &flon, &age);
  
  log_string(gps, "T"); //print normal characters and send gps date as test.
  log_string(gps, newtrack); //print if track begins
  log_float(flat, TinyGPS::GPS_INVALID_F_ANGLE, 10, 6);
  log_float(flon, TinyGPS::GPS_INVALID_F_ANGLE, 11, 6);
  log_float(gps.f_course(), TinyGPS::GPS_INVALID_F_ANGLE, 7, 2);
  log_float(gps.f_speed_kmph(), TinyGPS::GPS_INVALID_F_SPEED, 6, 2);
  log_date(gps);

  print_spd(gps.f_speed_kmph(), TinyGPS::GPS_INVALID_F_SPEED, 3, 3);
  print_head(gps.f_course(), TinyGPS::GPS_INVALID_F_ANGLE, 3, 0, 13);
  print_temp(temp, 1);
  print_log(gps.satellites(), TinyGPS::GPS_INVALID_SATELLITES, 1, 1, 15);

  smartdelay(1000);

}

static void smartdelay(unsigned long ms)
{
  unsigned long start = millis();
  do 
  {
    while (ss.available())
      gps.encode(ss.read());
  } while (millis() - start < ms);
}

static void print_spd(float val, float invalid, int len, int prec)
{
  if (val == invalid)
  {
    lcd.setCursor(0,0);
    while (len-- > 0)
      lcd.write('*');
    }
   
  else
  {
    lcd.setCursor(0,0);
    while (len-- > 0)
     lcd.write(' ');
    lcd.setCursor(0,0);
    char buf [3];
    dtostrf(val,3,0,buf);
    //sprintf(buf, "%f", val);
    lcd.write(buf);
    
  }
  smartdelay(0);
}


static void print_head(float val, float invalid, int len, int prec, int pos)
{
  if (val == invalid)
  {
    lcd.setCursor(pos,0);
    while (len-- > 0)
      lcd.write('*');
    }
   
  else
  {
    lcd.setCursor(pos,0);
    while (len-- > 0)
     lcd.write(' ');
    lcd.setCursor(pos,0);
    char buf [3];
    dtostrf(val,len,prec,buf);
    lcd.write(buf);
    int iDeg = int(val);

    int idegpos;
    idegpos = pos - 3;  

    lcd.setCursor(idegpos ,0);
    lcd.write(" ");
    lcd.setCursor(idegpos ,0);
    
    if (iDeg > 68 && iDeg < 112) {
    lcd.write("E");
  }
    if (iDeg > 23 && iDeg < 67) {
    lcd.write("NE");
  }
   if (iDeg > 338 && iDeg < 22) {
    lcd.write("N");
  }
   if (iDeg > 293 && iDeg < 337) {
    lcd.write("NW");
  }
   if (iDeg > 248 && iDeg < 292) {
    lcd.write("W");
  }
   if (iDeg > 202 && iDeg < 247) {
    lcd.write("SW");
  }
   if (iDeg > 158 && iDeg < 201) {
    lcd.write("S");
  }
   if (iDeg > 113 && iDeg < 157) {
    lcd.write("SE");
  }
    
  }
  
  smartdelay(0);
}

static void print_log(float val, float invalid, int len, int prec, int pos)
{
   if (stat == 0)
  {
    lcd.setCursor(pos,1);
    while (len-- > 0)
      lcd.write('C');
    }
 else
 {
  if (val == invalid)
   {
    lcd.setCursor(pos,1);
    while (len-- > 0)
     lcd.write(' ');
    lcd.setCursor(pos,1);
    lcd.write("S");
  }
  else
  {  lcd.setCursor(pos,1);
    while (len-- > 0)
     lcd.write(' ');
    lcd.setCursor(pos,1);
    lcd.write("A");
  }
 }
  smartdelay(0);
}

static void print_temp(int t, int pos)
{
    lcd.setCursor(pos,1);
     lcd.write("  ");
    lcd.setCursor(pos,1);
    String tmp = String(t);
    lcd.write("80");
    
  smartdelay(0);
}


static void log_float(float val, float invalid, int len, int prec)
{
   mySensorData = SD.open(filename, FILE_WRITE); //Open file on SD card for writing
  if (val != invalid)
    {
    char buf [len];
    dtostrf(val,len,prec,buf);
//    cardbuffer += buf;

    mySensorData.print(buf); //Write buffer to SD card
    mySensorData.print(", ");
//    Serial.print(buf);
  }
 mySensorData.close();  //Close the file  
  smartdelay(0);
}

static void log_date(TinyGPS &gps)
{
  mySensorData = SD.open(filename, FILE_WRITE); //Open file on SD card for writing
  int year;
  byte month, day, hour, minute, second, hundredths;
  unsigned long age;
  gps.crack_datetime(&year, &month, &day, &hour, &minute, &second, &hundredths, &age);
  if (age != TinyGPS::GPS_INVALID_AGE)
  {
    char sz[32];
    sprintf(sz, "%02d/%02d/%02d, %02d:%02d:%02d ",
        month, day, year, hour, minute, second);

//      cardbuffer += sz;
    mySensorData.print(sz);
    mySensorData.println("");
//    Serial.print(sz);
    
  }
//  log_int(age, TinyGPS::GPS_INVALID_AGE, 5);
  mySensorData.close();  //Close the file  
  smartdelay(0);
}


static void log_string(TinyGPS &gps, String text)
{
  int year;
  byte month, day, hour, minute, second, hundredths;
  unsigned long age;
  gps.crack_datetime(&year, &month, &day, &hour, &minute, &second, &hundredths, &age);
  mySensorData = SD.open(filename, FILE_WRITE); //Open file on SD card for writing
  if (age != TinyGPS::GPS_INVALID_AGE)
  {

    mySensorData.print(text);
    mySensorData.print(",");
    if (text == "1")
    {
      newtrack = "0";
    }
  }
  mySensorData.close();  //Close the file  
  smartdelay(0);
}

Thanks in advance

-Hop

So, everything's working ok, you just want a critique? Here ya' go: "It's not perfect." :smiley:

But it's working, so what's the difference? Are you going to add more stuff?

Or do you just have a vague sense that you need to "improve" it? Well, here's my list of suggested improvements, in no particular order:

  • Don't use String => Save 1600 bytes of program space and untold hours of wondering why it freezes after a random number of hours.
  • Use the F macro for string literals where possible => saves RAM
  • Fix libraries to take PROGMEM arguments => allows use of F macro everywhere, which saves RAM
  • Use NeoGPS instead of TinyGPS => configure to parse just the GPS elements and messages you use, eliminates the not-so-smart smartDelay, saves processing time and program space (I wrote NeoGPS)
  • Use NeoSWSerial instead of SoftwareSerial => increased reliability, and much, much more efficient (jboyton wrote NeoSWSerial, but I maintain it on my github page)
  • Quit opening and closing the SD file in the middle of a line => fixes broken lines
  • Don't use sprintf => save a few K in program space
  • Don't use float => save a few K in program space, use increased precision of integers, eliminate naughty use of "==" with floating-point values
  • Change print_head to use a PROGMEM table
    Remember, free advice is worth what you paid for it... :slight_smile:

Cheers,
/dev

I'll be watching this closely... Its a very similar problem (and code) to mine. /dev also pointed me in the same directions... Im currently looking at NeoGPS and NeoSWSerial to reduce my memory issues.

/dev:

  • Fix libraries to take PROGMEM arguments => allows use of F macro everywhere, which saves RAM

How would one go about doing this? My display library uses a LOT of memory because it cant use the "(F(" trick. Modifying the library sounds like way over my newbie head though (and probably hop's too cos he's also a newbie)!

Thx
J

How would one go about [fixing libraries to take PROGMEM arguments]?

There are (at least) two choices:

(1) modify the library and add an overloaded method that takes a PROGMEM argument. "Overloaded" means it has the same name (like print or write), but different argument types (const __FlashStringHelper * instead of const char *); or

(2) add a helper routine in your own code that calls an existing library call. In this case, the helper routine loads one byte from PROGMEM and calls the existing print(char).

The advantage of (1) is cleaner calling code... everything is a method of the library/class. The disadvantage is that if the author releases bug fixes, you have to merge his changes in to get the same fix, or take the new version of his library and re-add the PROGMEM methods.

The advantage of (2) is a smaller learning curve... it's just a function. The disadvantage is that sometimes you call the helper function (for F("args")) and other times you call the library (for ints).

Let's start with (1). Say the library only provides a print method for RAM-wasting char *, like "foo":

  void print( char * );

If you look in the CPP file, it is very likely that this method calls some other method that takes just one character at a time:

void LCD::print( char *str )
{
  for (char c = *str; c != '\0'; str++) // walk through the string until we hit the NUL terminator...
    print( c );         // ... and print each char
}

Well, if it can call that method, so can you! (Usually.) You can add a new print method to the H file that takes a PROGMEM argument:

  void print( const char *str );       // <-- existing method
  void print( char c );          // <-- existing method
  void print( const __FlashStringHelper *progmem_str );  // <-- new method

Then you have to implement it in the CPP as well:

void LCD::print( const __FlashStringHelper *progmem_str )
{
  const char *ptr = (const char *) progmem_str;

  for (;;) {
     char c = pgm_read_byte(ptr++); // get one char from PROGMEM...
     if (!c)
       break;
     print( c );             // ... and use the single-char method to print it
  }
}

BAM! Now you can do

  lcd.print( F("F-man saves RAM!") );

Now it's easy to explain approach (2). Just write a helper function without modifying the library:

void lcd_print_P( const __FlashStringHelper *progmem_str )
{
  const char *ptr = (const char *) progmem_str;

  for (;;) {
     char c = pgm_read_byte(ptr++); // get one char from PROGMEM...
     if (!c)
       break;
     lcd.print( c );             // ... and print it
  }
}

Then you call it from your sketch like this:

  lcd_print_P( F("FLASH!  F-man saves RAM again!") );

Voilà!

Cheers,
/dev

If you remove power while the logger is saving files, then you will get a broken line. The only way to avoid it is to not randomly remove power. Here is an idea. You have two power sources, the car, and a backup battery. When the car is running and supplies enough voltage, arduino draws power from the car. Then if the car engine stops, arduino draws power from the backup battery long enough to finish saving the file and halt. To do this, you can power your arduino using a cigarette lighter to power barrel adapter. Then also connect a power bank to arduino USB. Arduino's power selection circuit will use power from the barrel if it is present, so the backup battery doesn't drain. Have a resistor voltage divider to drop Vin (aka power barrel) from 12V to less than 5V and connect it to A0 (google arduino battery monitor). This way after every complete data set, you check A0 to determine whether to keep logging (Vin has enough voltage) or halt (Vin has less than 5V).

So, everything's working ok, you just want a critique? Here ya' go: "It's not perfect." :smiley:

<- LOL!

Thanks for the advice, very constructive. This was only my first try and I am already running into limits. The code was based on the TinyGPS "test with GPS device" example code. I was planning on adding some more functions (a switch that toggles between MPH and KMH) however I first need to do some optimizing. (already experienced some instability problems)

I'll create a alternative program based on your comment, this will probably take me a while. Wish me luck.

@liudr
I will also look into the backup idea. However this would mean setting up a pin that detects witch power source is being used. And I would have to explore that option. Thanks for the idea.

-Hop

  float flat, flon;
  unsigned long age, date, time, chars = 0; 
  gps.f_get_position(&flat, &flon, &age);

Even if there is no GPS data? That smartdelay() function is the dumbest possible way to read GPS data.

The TinyGPS++ library comes with a DeviceExample example that shows how to properly read, and use, GPS data.

Until you change how you read data, and understand when there is data to use, the rest of your code is crap or irrelevant (whichever term you prefer).

@PaulS

thanks for your input, I am still learning and I will look into what the example uses that line for.

hoppend:
I started a project after a lot of research to create a GPS logger with an LCD screen. I want to use this device to monitor the speed in my classic car.

For a car you possibly want bigger digits.
This is also possible with 1602 text LCDs and using "big numbers", created from user created special chars:

Perhaps you can switch from RPM to speed automatically every two seconds or so, as there will fit only a few digits on the display when using big numbers.

hoppend:
At the same time it would be nice to plot a track.

I'd possibly provide a file export to Serial in KML data format which can be used with Google Maps on a PC to plot a track along with either Google Earth view or Google Streetmap view. Or provide a file converting routine from log file to KML file on the Arduino itself.

hoppend:
After a lot of work I have finally got my Arduino gps logger working on my own. However the code might not be the most optimal. And with turning on and of of the arduino it starts logging in the middle of the previous (broken) line in the file on the SD card.

I think such broken lines could be prevented easily in almost every case:
Instead of programming logic

  • open file
  • write a small part of the line
  • close file
    use a programming logic like:
  • create a full line
  • log a full line to SD card

Most likely I'd completely avoid using such thing like a "GPS library", and instead write a couple of functions, that

  • read one line of NMEA data
  • and if the line has a $GPRMC prefix, log that line to SD card
    (perhaps every 10 seconds or so, when logging is active)

jurs:
Most likely I'd completely avoid using such thing like a "GPS library", and instead write a couple of functions, that

  • read one line of NMEA data
  • and if the line has a $GPRMC prefix, log that line to SD card

Hmmm... that seems a little too simplistic. How would he display the fields without parsing them out? Is it ok to log lines or use fields with a CRC error, or if the GPS device doesn't have a fix yet?

Sounds like you like to use PC tools to process the raw NMEA data. If he were just logging to an SD card, then, yes, he could just save sentences, a much simpler problem.

The same could be said about exporting to KML. If he doesn't parse the numbers out, how will he write them out in the KML format? He could do a conversion on the PC, if he were to save just the raw NMEA data, but that doesn't provide speed and heading to display on the LCD.

Cheers,
/dev

Most likely I'd completely avoid using such thing like a "GPS library", and instead write a couple of functions, that

  • read one line of NMEA data
  • and if the line has a $GPRMC prefix, log that line to SD card

I think the simplest way would be to log to a Unicode CSV file.

I will use the parsed data to display to the lcd and log to the SD card if there is a fix. Looks like the NEOgps library would be able to provide me with a way to determine if the gps has a fix.

hoppend:
I think the simplest way would be to log to a Unicode CSV file.

Yes, CSV files are easy to log.

But "to plot a track" you will probably need a KML file for use with Google Maps on your PC. If you had logged a KML file (instead or in addition to a CSV file), you could take the SD card from your Arduino, put it in a PC with Google Maps installed, and watch your track in sattelite view or road map view as you like.

hoppend:
I will use the parsed data to display to the lcd and log to the SD card if there is a fix. Looks like the NEOgps library would be able to provide me with a way to determine if the gps has a fix.

Parsing a NMEA line of data is not that complicated. All the data you need (GPS latitude/longitude, date, time, active fix indicator, course, speed) are included in lines starting with " $GPRMC", where GP stand for "GPS" and RMC for "recommended minimum sentence C". Like in the following line:

 $GPRMC,225446,A,4916.45,N,12311.12,W,000.5,054.7,191194,020.3,E*68

The 'A' in that line would indicate an "active location fix".

/dev:
So, everything's working ok, you just want a critique? Here ya' go: "It's not perfect." :smiley:

But it's working, so what's the difference? Are you going to add more stuff?

Or do you just have a vague sense that you need to "improve" it? Well, here's my list of suggested improvements, in no particular order:

  • Don't use String => Save 1600 bytes of program space and untold hours of wondering why it freezes after a random number of hours.
  • Use the F macro for string literals where possible => saves RAM
  • Fix libraries to take PROGMEM arguments => allows use of F macro everywhere, which saves RAM
  • Use NeoGPS instead of TinyGPS => configure to parse just the GPS elements and messages you use, eliminates the not-so-smart smartDelay, saves processing time and program space (I wrote NeoGPS)
  • Use NeoSWSerial instead of SoftwareSerial => increased reliability, and much, much more efficient (jboyton wrote NeoSWSerial, but I maintain it on my github page)
  • Quit opening and closing the SD file in the middle of a line => fixes broken lines
  • Don't use sprintf => save a few K in program space
  • Don't use float => save a few K in program space, use increased precision of integers, eliminate naughty use of "==" with floating-point values
  • Change print_head to use a PROGMEM table
    Remember, free advice is worth what you paid for it... :slight_smile:

Cheers,
/dev

So I have re-written my sketch to use the NeoGPS code, now I have a question: What would be the best way of getting the RMC sentence to my SD card. I can combine all the parsed data in a string and write that to the card. However I can Imagine that there is a faster way.

-Hoppend

However I can Imagine that there is a faster way.

The NeoGPS instance maintains the complete sentence. If there is not a method in the class to return the (pointer to the) sentence, add one.

Sorry, I have just started getting into arduino's and coding in C.

But I can not find an example of getting the sentance so I do not know where to start. I did get all the other data I need for my project.

hoppend:
What would be the best way of getting the RMC sentence to my SD card.

What do you plan on doing with the SD card? If it's another application that you are writing, there are several choices: text (as you're doing now) or binary (much quicker and smaller).

If it's a PC application that you can't modify, it might be best to have the Arduino write it to the SD card in the required format: KML or NMEA.

But if the format is verbose, or requires some computation that would take too long on the Arduino, you may need to write a bridging application on your PC to convert the SD card data to the complex format. It would be easiest to have the Arduino write the GPS data in text or binary format.

You really have to look at it from both sides of the SD card: how you read it will probably influence how you shoud write it.

Cheers,
/dev

/dev:
If it's a PC application that you can't modify, it might be best to have the Arduino write it to the SD card in the required format: KML or NMEA.

Kml or nmea would be nice. However I was planning on producing a comma seperated file with the arduino. I just want to be able to produce a route on google maps. so I can convert te file to kml later.

The NeoGPS instance maintains the complete sentence.

All libraries except NeoGPS maintain anywhere from the last field to two sentences. NeoGPS only maintains the parsed ints and enums, not the raw chars and not even floats, depending on what has been enabled with the configuration header file. Disabling sentences or fields removes the corresponding ints or enums (to save RAM) and causes those received characters to be skipped (to save time). Compared to other libraries, NeoGPS saves 140-1100 bytes of RAM, and uses 1/4 to 2/3 of the CPU time.

If there is not a method... add one.

I have considered adding a "format" method that would use the parsed information to create an NMEA sentence. This would allow an Arduino to emulate a GPS device, perhaps using a script (for simulation) or other sensors (to increase accuracy). Done properly, this should not increase RAM usage, nor affect parsing performance.

I have also considered adding a SAVE_RAW_SENTENCE configuration item. At least the user could choose to use the RAM for this purpose, unlike other libraries.

Cheers,
/dev

I was planning on producing a comma separated file with the Arduino.

Ok, that's really what the streamers H and CPP files are doing. If you just want to save the pieces from gps.fix(), like date/time, lat/lon, speed, alt, et al, you can just do this:

    sdFile << gps.fix(); // or whatever fix copy you might be using
    sdFile.println();

That's all you would need on the SD card to reconstruct your route.

But if you also want the entire satellite constellation and received data stats (char, sentences, checksum errors), I've been working on a change that would let you redirect all that output to a different Stream, like an SD file. If you can wait a day or so, I'll check something in for you. If you're ready now, you can cut and paste the body of trace_all from those files into your app, then modify it to use your sdFile instead of trace, something like this:

void log_to( Stream &log )
{
  log << gps.fix(); // or whatever fix copy you might be using

  #if defined(NMEAGPS_PARSE_SATELLITES)
    log << '[';

    for (uint8_t i=0; i < gps.sat_count; i++) {
      log << gps.satellites[i].id;

      #if defined(NMEAGPS_PARSE_SATELLITE_INFO)
        log << ' ' << 
          gps.satellites[i].elevation << '/' << gps.satellites[i].azimuth;
        log << '@';
        if (gps.satellites[i].tracked)
          log << gps.satellites[i].snr;
        else
          log << '-';
      #endif

      trace << ',';
    }

    log << F("],");
  #endif

  #ifdef NMEAGPS_STATS
    log << gps.statistics.ok         << ','
          << gps.statistics.crc_errors << ','
          << gps.statistics.chars      << ',';
  #endif

  log << '\n';

} // log_to

Then just call this routine from the proper place, like this:

    log_to( sdFile );

It sounds like you don't really need these extra bits. The first technique is probably sufficient.

BTW, if you enable (or disable) something later, both techniques will still work and will write more (or less) data.

Cheers,
/dev