Go Down

Topic: NeoGPS - configurable, ultra-small RAM footprint (Read 16741 times) previous topic - next topic

-dev

After wrestling with the terrible NMEA protocol for the nth time in my career, and realizing how many Arduino libraries are so wasteful of RAM, I decided to write a fully configurable GPS library, NeoGPS.  It is positively stingy with RAM.  Tight-fisted, even.  :)

  • The minimal NeoGPS configuration requires only 10 bytes
  • The full NeoGPS configuration requires only 43 bytes
The best implementation I've found so far is TinyGPS, and it has been a great inspiration.  It uses 120 bytes per instance plus another 60 bytes of character data.  A similarly-configured NeoGPS requires 73 bytes.

TinyGPS is also a great example of how to support different program structures.  Most people will use it in a polled fashion: check for serial byte, then process serial byte.  But it also supports serial interrupt processing, even though the Arduino framework does not.  (google for "Arduino Serial Interrupt" to get an idea of that general approach... it requires modifying HardwareSerial.cpp)

Most libraries have extra buffers so that parts of the sentence can be parsed all at once.  For example, an extra field buffer may hold on to all the characters between commas.  That buffer is then parsed into a single data item, like heading.  Some libraries even hold on to the entire sentence before attempting to parse it.  This also requires extra CPU time to copy the bytes and index through them... again.

  • NeoGPS parses each character immediately into the data item.  When the delimiting comma is received, the data item has been fully computed and is marked as valid.
Most libraries parse all fields of their selected sentences.  Although most people use GPS for obtaining lat/long, some need only time, or even just one pulse-per-second.

  • NeoGPS configures each item separately.  Disabled items are conditionally compiled, which means they will not use any RAM, program space or CPU time.
Most libraries treat sentences as independent from each other.  A long-running frustration has been that multiple GPS sentences must be received in order to know all the information about the current fix: position, velocity, accuracy, etc.

  • NeoGPS can group information from multiple sentences into one fix.
If you need a coherent fix, you must also correlate the sentences by time.  By coherent, I mean that the Position at a particular time is grouped with the Velocity at that same time.  BTW, Some sentences have time, some do not.  Who thought this up, anyway?  :smiley-mad:

  • NeoGPS can correlate multiple sentences into one coherent fix.
Most libraries require float-point support.  This can be caused by their data types, the use of scanf, or even distance/heading calculations.

  • NeoGPS uses integer representations for all members, preserving their full accuracy.
Optional accessors can convert the members to floating-point, if that's what you really need.

Furthermore, GPS device manufacturers have recognized the inherent deficiencies in the NMEA protocol, and frequently provide their own proprietary NMEA sentences or even their own protocol.

  • Classes can be derived from NeoGPS to implement additional protocols or NMEA sentences.
My particular device is the u-blox NEO-6M, so I have provided a derived class ubloxNMEA for its proprietary NMEA sentences.  For most devices, it should be as simple as identifying the sentences in a table and parsing the specific types in parseField

I have also provided a second derived class ubloxGPS for the UBX protocol.  This class shows how to chain multiple protocols from the same device.  It also shows how to accumulate a fix as it is received, without any buffering.

Tradeoffs

There's a price for everything, hehe...

  • Parsing without buffers, or in place, means that you must be more careful about when you access data items.
In general, you should wait to access the fix until after the entire sentence has been parsed (See loop() in NMEA.ino).  Member function coherent() can also be used to determine when it is safe.  If you need to access the fix at any time, you will have to double-buffer the fix  (See NMEAGPS.h comments regarding a safe_fix).  Also, data errors can cause invalid field values to be set before the CRC is fully computed.  The CRC will catch most of those, and the fix members will be marked as invalid at that time.

  • Configurability means that the code is littered with #ifdef sections.
I've tried to increase white space and organization to make it more readable, but let's be honest... conditional compilation is ugly.

  • Accumulating parts of a fix into group means knowing which parts are valid.
Before accessing a part, you must check its valid flag.  Fortunately, this adds only one bit per member.  See GPSfix.cpp for an example of accessing every data member.

  • Correlating timestamps for coherency means extra date/time comparisons for each sentence before it is fused.
See NMEAfused.ino for code that determines when a new time interval has been entered.

  • Full C++ OO implementation is more advanced than most Arduino libraries, but it's is a good way to support future capabilities.
You've been warned!  ;)

  • "fast, good, cheap... pick two."
Although most of the RAM reduction is due to eliminating buffers, some of it is from trading RAM for additional code.  And, as I mentioned, the readabilty (i.e., goodness) suffers from its configurability.

Well, if you're super constrained by RAM or need better performance or fix fusion, please take a look.

Cheers,
/dev

P.S. There are a few things I will be adding shortly: GPGST, GPGSA, and GPGSV sentences, along with their field types, and maybe some of the most popular proprietary sentences (with derived classes).

P.P.S.  I am currently using 1.0.5

-dev

The GPGST, GPGSA and GPGSV sentences are now implemented.  They provide VDOP, PDOP, lat/lon/alt errors in centimeters, and information about the satellite constellation.

I also gathered some performance and PROGMEM stats.  As I suspected, the NeoGPS CPU time per sentence is about two-thirds that of TinyGPS: about 1000uS vs 1400uS.

For the same configuration, NeoGPS takes a little more, 2800 bytes of program space, while TinyGPS takes 2400 bytes.  NeoGPS can be configured to be as small as 866 bytes or as large as 3492 bytes.  Please see the README for a complete breakdown.

Cheers,
/dev

jboyton

Does the library require a UART? On an Uno, I have to use some form of software serial to communicate with a GPS.

-dev

No, it just requires a byte.  :)  I think I see two left-overs from my debugging that imply the need for UART or Serial... not necessary.  I'll go nix those.

All the example programs use Serial for debug output and Serial1 for reading GPS bytes.  Those bytes are then given to the library.  Internally, NeoGPS doesn't do anything with a UART -- it just uses the bytes passed in.

In your case, you'll do a SoftwareSerial read to get a byte.  Then pass that byte to gps.decode just like in the examples.  Something like this:

Code: [Select]

SoftwareSerial ss( rxpin, txpin );
...
void loop()
{
  while (ss.available())
    if (gps.decode( ss.read() ) == NMEAGPS::DECODE_COMPLETED) {

      //  Got a sentence, do something with gps.fix()...



If you need to send something to the GPS device, like a configuration or reset command, just pass your SoftwareSerial instance to send:

Code: [Select]

   // ask for a PUBX sentence
   gps.send_P( &ss, F("PUBX,00") );


Cheers,
/dev

jboyton

#4
Jan 05, 2015, 09:13 am Last Edit: Jan 05, 2015, 09:14 am by jboyton
Glad to hear that. There's a code snippet in the readme file that refers to "uart1.available()" which was why I wondered.

I tried all of your examples with "NMEA" as part of the filename. None of these produced any output of interest to me, just a few lines about an object size. I was hoping for a demo that would print out parsed GPS values, like the TinyGPS example does.

I wanted to give it a spin and compare it to what I'm currently doing. But I'm not sure how to use this library.

-dev

> None of these produced any output of interest to me, just a few lines about an object size.

Hmmm, all you're seeing is the startup banner.  Like you expected, it should print out the parsed GPS values after a GPRMC sentence is received.  Could you give me a little information about your setup?  I see you're using an Uno, but what GPS device do you have?

The default configuration is just like TinyGPS: it only parses GPGGA and GPRMC.  If you simply reload with TinyGPS and it works, your device must put out one or both of those.  I suspect there's something different about your Software Serial.  Maybe check the 9600 baud rate.  Could you post your version of NMEA.ino here so I could give it a try?

Here's one thing to test: in loop(), move the sentence_received() call above the if statement:

Code: [Select]

      sentenceReceived(); // moved
      if (gps.nmeaMessage == NMEAGPS::NMEA_RMC) {

        //  Use received GPRMC sentence as a pulse
        seconds++;
      }


If you start getting output, that would mean your device is not emitting a GPRMC.  That would be unusual, so I really suspect the SoftwareSerial.

Thanks,
/dev

jboyton

#6
Jan 05, 2015, 05:56 pm Last Edit: Jan 05, 2015, 05:57 pm by jboyton
I have a Global Top PA6H GPS module.

The Arduino SoftwareSerial doesn't work very well so I wrote my own version. What specific requirements does your GPS code have in the way of a serial library? I've been using this software serial at 38400 baud in a GPS logging sketch without issue.

For testing with your library I programmed the GPS to output GGA and RMC once per second at 9600 baud. I tried moving the sentence_received() call but it had no effect.
Here is NMEA.ino:

Code: [Select]
#include <zSoftSerial.h>
zSoftSerial Serial1;

/*
  Serial is for trace output.
  Serial1 should be connected to the GPS device.
*/

#include <Arduino.h>

#include "NMEAGPS.h"
#include "Streamers.h"

// Set this to your debug output device.
Stream & trace = Serial;

static uint32_t seconds = 0L;

static NMEAGPS gps;

//--------------------------

static void sentenceReceived()
{
#if !defined(GPS_FIX_TIME) & !defined(GPS_FIX_DATE)
  //  Date/Time not enabled, just output the interval number
  trace << seconds << ',';
#endif

  trace << gps.fix();

#if defined(NMEAGPS_PARSE_SATELLITES)
  if (gps.fix().valid.satellites) {
    trace << ',' << '[';

    uint8_t i_max = gps.fix().satellites;
    if (i_max > NMEAGPS::MAX_SATELLITES)
      i_max = NMEAGPS::MAX_SATELLITES;

    for (uint8_t i=0; i < i_max; i++) {
      trace << gps.satellites[i].id;
#if defined(NMEAGPS_PARSE_SATELLITE_INFO)
      trace << ' ' <<
        gps.satellites[i].elevation << '/' << gps.satellites[i].azimuth;
      trace << '@';
      if (gps.satellites[i].tracked)
        trace << gps.satellites[i].snr;
      else
        trace << '-';
#endif
      trace << ',';
    }
    trace << ']';
  }
#endif

  trace << '\n';

} // sentenceReceived

//--------------------------

void setup()
{
  // Start the normal trace output
  Serial.begin(9600);
  trace.print( F("NMEA test: started\n") );
  trace.print( F("fix object size = ") );
  trace.println( sizeof(gps.fix()) );
  trace.print( F("NMEAGPS object size = ") );
  trace.println( sizeof(NMEAGPS) );
  trace.flush();
  
  // Start the UART for the GPS device
  Serial1.begin(9600);
}

//--------------------------

void loop()
{
  while (Serial1.available())
    if (gps.decode( Serial1.read() ) == NMEAGPS::DECODE_COMPLETED) {
//      trace << (uint8_t) gps.nmeaMessage << ' ';

// Make sure that the only sentence we care about is enabled
#ifndef NMEAGPS_PARSE_RMC
#error NMEAGPS_PARSE_RMC must be defined in NMEAGPS.h!
#endif

        sentenceReceived();    // moved from below
      if (gps.nmeaMessage == NMEAGPS::NMEA_RMC) {
//        sentenceReceived();

        //  Use received GPRMC sentence as a pulse
        seconds++;
      }
    }
}



Here is a simple sketch that verified that the GPS was outputting correctly (it was):

Code: [Select]
#include <zSoftSerial.h>
zSoftSerial Serial1;

void setup() {
  Serial.begin(9600);
  Serial1.begin(9600);
}

void loop() {
  while (Serial1.available())
    Serial.write(Serial1.read());
}

jboyton

Here is a sketch with TinyGPS which works well:

Code: [Select]
#include <zSoftSerial.h>
zSoftSerial ss;

#include <TinyGPS.h>
TinyGPS gps;

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);


void setup() {
  Serial.begin(9600);
  ss.begin(9600);
  Serial.print("Testing TinyGPS library v. "); Serial.println(TinyGPS::library_version());
  Serial.println("by Mikal Hart");
  Serial.println();
  Serial.println("Sats HDOP Latitude  Longitude  Fix  Date       Time     Date Alt    Course Speed Card  Distance Course Card  Chars Sentences Checksum");
  Serial.println("          (deg)     (deg)      Age                      Age  (m)    --- from GPS ----  ---- to London  ----  RX    RX        Fail");
  Serial.println("-------------------------------------------------------------------------------------------------------------------------------------");


}

void loop()
{
  float flat, flon;
  unsigned long age, date, time, chars = 0;
  unsigned short sentences = 0, failed = 0;
  static const double LONDON_LAT = 51.508131, LONDON_LON = -0.128002;
 
  print_int(gps.satellites(), TinyGPS::GPS_INVALID_SATELLITES, 5);
  print_int(gps.hdop(), TinyGPS::GPS_INVALID_HDOP, 5);
  gps.f_get_position(&flat, &flon, &age);
  print_float(flat, TinyGPS::GPS_INVALID_F_ANGLE, 10, 6);
  print_float(flon, TinyGPS::GPS_INVALID_F_ANGLE, 11, 6);
  print_int(age, TinyGPS::GPS_INVALID_AGE, 5);
  print_date(gps);
  print_float(gps.f_altitude(), TinyGPS::GPS_INVALID_F_ALTITUDE, 7, 2);
  print_float(gps.f_course(), TinyGPS::GPS_INVALID_F_ANGLE, 7, 2);
  print_float(gps.f_speed_kmph(), TinyGPS::GPS_INVALID_F_SPEED, 6, 2);
  print_str(gps.f_course() == TinyGPS::GPS_INVALID_F_ANGLE ? "*** " : TinyGPS::cardinal(gps.f_course()), 6);
  print_int(flat == TinyGPS::GPS_INVALID_F_ANGLE ? 0xFFFFFFFF : (unsigned long)TinyGPS::distance_between(flat, flon, LONDON_LAT, LONDON_LON) / 1000, 0xFFFFFFFF, 9);
  print_float(flat == TinyGPS::GPS_INVALID_F_ANGLE ? TinyGPS::GPS_INVALID_F_ANGLE : TinyGPS::course_to(flat, flon, LONDON_LAT, LONDON_LON), TinyGPS::GPS_INVALID_F_ANGLE, 7, 2);
  print_str(flat == TinyGPS::GPS_INVALID_F_ANGLE ? "*** " : TinyGPS::cardinal(TinyGPS::course_to(flat, flon, LONDON_LAT, LONDON_LON)), 6);

  gps.stats(&chars, &sentences, &failed);
  print_int(chars, 0xFFFFFFFF, 6);
  print_int(sentences, 0xFFFFFFFF, 10);
  print_int(failed, 0xFFFFFFFF, 9);
  Serial.println();
 
  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_float(float val, float invalid, int len, int prec)
{
  if (val == invalid)
  {
    while (len-- > 1)
      Serial.print('*');
    Serial.print(' ');
  }
  else
  {
    Serial.print(val, prec);
    int vi = abs((int)val);
    int flen = prec + (val < 0.0 ? 2 : 1); // . and -
    flen += vi >= 1000 ? 4 : vi >= 100 ? 3 : vi >= 10 ? 2 : 1;
    for (int i=flen; i<len; ++i)
      Serial.print(' ');
  }
  smartdelay(0);
}

static void print_int(unsigned long val, unsigned long invalid, int len)
{
  char sz[32];
  if (val == invalid)
    strcpy(sz, "*******");
  else
    sprintf(sz, "%ld", val);
  sz[len] = 0;
  for (int i=strlen(sz); i<len; ++i)
    sz[i] = ' ';
  if (len > 0)
    sz[len-1] = ' ';
  Serial.print(sz);
  smartdelay(0);
}

static void print_date(TinyGPS &gps)
{
  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)
    Serial.print("********** ******** ");
  else
  {
    char sz[32];
    sprintf(sz, "%02d/%02d/%02d %02d:%02d:%02d ",
        month, day, year, hour, minute, second);
    Serial.print(sz);
  }
  print_int(age, TinyGPS::GPS_INVALID_AGE, 5);
  smartdelay(0);
}

static void print_str(const char *str, int len)
{
  int slen = strlen(str);
  for (int i=0; i<len; ++i)
    Serial.print(i<slen ? str[i] : ' ');
  smartdelay(0);
}

-dev

> The Arduino SoftwareSerial doesn't work very well so I wrote my own version.

Awesome.  You're smarter than the average bear!  :D

> What specific requirements does your GPS code have in the way of a serial library?

None, really.  Just "gimme a byte whenever."

Since you confirmed that the device is sending those two sentences, I suspect they are being rejected for some reason.  CRC comes to mind: I've encountered what I thought was an invalid example sentence in the manufacturer document.  Or perhaps NeoGPS is rejecting a field value.  I also noticed that TinyGPS does not reject characters outside the range ' '..'~', while NeoGPS does.

> I have a Global Top PA6H GPS module.

I just tried some example sentences from their manual, and they are being rejected.   :-[  We are experiencing technical difficulties, please stand by...

Could you paste some of the output from your simple echo program?  Just to double check...

Thanks,
/dev

-dev

Ok, it wasn't real obvious, but I found this in their manual:

Name    |   Example    |   Units    |   Description
UTC Time   |   064951.000 |    |   hhmmss.sss

Looks fine, yes?  Grrr... there is one extra digit of precision: .SSS.  They track to milliseconds.  While TinyGPS discards the digit, I reject it as an invalid value.

I'll check in a version that does the same and review some of the other rejections.  Then I think I'll increase the precision of *NeoGPS* time to milliseconds.  I always disliked centiseconds, anyway.  ;)

Cheers,
/dev

jboyton

#10
Jan 05, 2015, 08:07 pm Last Edit: Jan 05, 2015, 08:08 pm by jboyton
I only bother with tenths of a second. This GPS maxes out at 10 Hz and with the lag between fix and transmission and then decode I don't think a precision of 10ms is very useful, never mind 1ms.

I haven't spent that much time looking into it but I know of at least one other GPS that reports the time to a precision of milliseconds (u-blox). I seem to recall running across one that output a greater precision of lat/lon data as well. I got the feeling that the NMEA strings are a loose standard and any decoding software would either have to know which device it was working with or be very flexible and forgiving.

-dev

Fixt.  There were 3 or 4 other fields that were a little restrictive, so I relaxed those as well.  Please start with NMEA, NMEAfused or NMEAtest.  I've got some merging to do on the others.

> a precision of milliseconds (u-blox)

Yes, that's the unit I have, but ms only appear in the UBX protocol time-of-week.  The format of their NMEA time field is declared to be .SSS, but the examples (and the device) use .SS

> I got the feeling that the NMEA strings are a loose standard...

...using the term loosely.

> I don't think a precision of 10ms is very useful, never mind 1ms.

Certainly not for anything I would do.  I am thinking about tossing this over to the ArduPilot boys.  I think I saw that they parse mS into 5Hz buckets, so even they don't care about 100Hz.

However, they might appreciate the extra 500uS/sentence or so that you can get with my techniques.  Their code appears to be based on TinyGPS.  Maybe after a few more people have tried it.

Cheers,
/dev

jboyton

I can believe that somebody wants that precision; just not me. Actually, the module I have has a very precise (±10ns) 1 Hz pulse output that is coincident with the GPS time. If someone were using that signal a 1ms precision might be wanted.

I downloaded your latest code and it is indeed spitting out what appears to be the fields from the RMC.

But I have to confess that I still don't know how to use your library. My C++ skills are very poor and I'm used to using crude techniques like Serial.println(something).

Suppose I want to print out the GPS speed from the RMC and the altitude from the GGA string. What do I have to do?

-dev

> But I have to confess that I still don't know how to use
> your library. My C++ skills are very poor and...


Yes, it's definitely your fault!   :)  Actually, that's very gracious of you.  Writing libraries for reuse is very hard, and I'm obviously deficient in documenting it.  The first "reuser" inevitably suffers through not being able to read the developer's mind.  Let's see...

I think the key concept is a fix.  A fix is the collective term for everything that's known about the physical state.  By "physical", I really mean the physics terms: position, velocity and acceleration.

GPS devices figure out position in terms of Latitude, Longitude and Altitude (or Earth-Centered x, y and z);  and velocity in terms of Speed and Heading (or Velocity North, East and down).  Some GPS devices have extra sensors (or interfaces to extra sensors) that provide acceleration.  Of course many projects can 'fuse' information from an IMU, barometer, compass, etc. to calculate these into one fix.

Unfortunately, the NMEA sentences don't provide a fix in one sentence.  We programmers have the delightful task of parsing multiple sentences in order to accumulate all the parts of a fix.  You have already learned that Speed is in the RMC and the Altitude is in the GGA:

> How do I print out the GPS speed from the RMC and the altitude from the GGA?

To answer your specific question: NeoGPS parses each sentence into gps.fix(), and a speed field gets parsed into gps.fix().speed.  The gps.fix().valid.speed flag is also set to true so you know which members have been filled out.  (See GPSfix.cpp for an example of accessing each member.)  So you should write something like this:

Code: [Select]
 while (ss.available())
    if (gps.decode( ss.read() ) == NMEAGPS::DECODE_COMPLETED) {

      if (gps.nmeaMessage == NMEAGPS::NMEA_RMC) {
        if (gps.fix().valid.altitude)
          Serial.println( gps.fix().altitude_cm() / 100 ); // meters
      } else if (gps.nmeaMessage == NMEAGPS::NMEA_GGA) {
        if (gps.fix().valid.speed)
          Serial.println( gps.fix().speed_mkn() / 1000 ) // knots
      }
    }
  }

I structured NeoGPS so that you could get just these two pieces, if that's all you want.  No extra copies, no extra buffers, no time wasted parsing lat/lon, etc.  All other libraries have those inefficiencies.

Truly, if you use those values immediately, that is all you should have to do.

  • Warning: if you remember the Tradeoffs section in the README, you'll know that you can't access gps.fix() at any other time than after a DECODE_COMPLETED.  Those values could be only half-parsed from the incoming bytes; they would be nonsense.
However... (TL;DR on its way)

...many programs need to use multiple parts at the same time (or at any time).

Let's say you didn't want to print one line for alt and a separate line for speed.  You'd probably like them on one line.  So you have to save them one at a time, and then print them together at a later time:

Code: [Select]
  uint32_t alt;
  uint32_t speed;

  while (ss.available()) {
    if (gps.decode( ss.read() ) == NMEAGPS::DECODE_COMPLETED) {

      if (gps.nmeaMessage == NMEAGPS::NMEA_RMC) {
        if (gps.fix().valid.altitude)
          alt = gps.fix().altitude_cm() / 100; // meters
      } else if (gps.nmeaMessage == NMEAGPS::NMEA_GGA) {
        if (gps.fix().valid.speed)
          speed = gps.fix().speed_mkn() / 1000; // knots
  } } }

  if (time_to_print())
    Serial.print( speed );
    Serial.print( F(", ") )
    Serial.println( alt );
  }

Ok, not too bad.  We have saved two parts, and they can be used at any time.

What if you want to be sure that the speed was measured at the same time as the altitude?  Then you've got to look at the time stamps.  Date and Time are parsed into gps.fix().dateTime

Code: [Select]
  uint32_t  alt;
  uint32_t  speed;
  dateTime  last_dt;
  uint8_t   parts = 0;

  while (ss.available()) {
    if (gps.decode( ss.read() ) == NMEAGPS::DECODE_COMPLETED) {

      if (gps.fix().valid.time) {
        if (last_dt != gps.fix().dateTime) {
          //  We're in the next time interval, print out
          //     the parts from the last interval if I got them.
          if (parts == 2) {
            Serial.print( speed );
            Serial.print( F(", ") )
            Serial.println( alt );
          }
          parts = 0;
          last_dt = gps.fix().dateTime;
        }
      }

      if (gps.nmeaMessage == NMEAGPS::NMEA_RMC) {
        alt = gps.fix().altitude_cm() / 100; // meters
        parts++;
      } else if (gps.nmeaMessage == NMEAGPS::NMEA_GGA) {
        speed = gps.fix().speed_mkn() / 1000; // knots
        parts++;
      }
    }
  }

Urk.  Add in the GPS status (tracking or not) and the code just gets uglier.

I think we can avoid all this mess. 

First, let's ignore the whole sentence type:

Code: [Select]
  uint32_t alt;
  uint32_t speed;

  while (ss.available()) {
    if (gps.decode( ss.read() ) == NMEAGPS::DECODE_COMPLETED) {

      if (gps.fix().valid.speed)
        speed = gps.fix().speed_mkn() / 1000;
      if (gps.fix().valid.altitude)
        alt = gps.fix().altitude_cm() / 100;

If the speed was received (in any message), we'll save it for later.  In general, check gps.fix().valid.XXX before you try to get the value for gps.fix().XXX().

Notice that we had to declare extra variables to hold the speed and altitude.  You'll need an extra copy of all the fix members that you care about.  Well, why don't we just use an instance of gps_fix?

Code: [Select]
gps_fix myFix;

void loop()
{
  while (ss.available()) {
    if (gps.decode( ss.read() ) == NMEAGPS::DECODE_COMPLETED) {

      if (gps.fix().valid.speed) {
        myFix.speed = gps.fix().speed;
        myFix.valid.speed = true;
      }
      if (gps.fix().valid.altitude) {
        myFix.altitude = gps.fix().altitude;
        myFix.valid.altitude = true;
      }

Wait, there are a whole bunch of fix members.  How about a subroutine to copy them all at once?  Just the valid ones, that is.  Yes, there is a method to do that.  It's a C++ operator, though, so the name is a little weird, and it's invoked like a C operator:

Code: [Select]
gps_fix myFix;

void loop()
{
  while (ss.available())
    if (gps.decode( ss.read() ) == NMEAGPS::DECODE_COMPLETED)
      myFix |= gps.fix();  // <-- calls the copy routine "operator |="

  if (time_to_print()) {
    if (myFix.valid.speed)
      Serial.print( myFix.speed_mkn() / 1000 );
    Serial.print( F(", ") );
    if (myFix.valid.altitude)
      Serial.println( myFix.altitude.whole );
  }

Mmmmm, much nicer!  This is approximately how most libraries work, and this is how NMEAfused.ino works.  They have an extra copy for use at any time.  When a sentence is parsed sucessfully, the new values are copied.

BTW, I have not seen any libraries that provide coherent fixes.  Please see NMEAcoherent.ino for the that technique.

More than you wanted to know?  :)  Well, I hope that helps, or leads to another question.

Cheers,
/dev

P.S.  I think I need a Configuration section in the README to describe how to enable and disable the messages and parts of a fix.  Real Soon Now.

jboyton

#14
Jan 06, 2015, 02:41 am Last Edit: Jan 06, 2015, 02:41 am by jboyton
More than you wanted to know?  :)  Well, I hope that helps, or leads to another question.
Less than what I wanted to know, actually. I can understand why documentation is frequently left for last, or left off entirely.

But I think I know enough now to compare your library to what I'm currently doing.

Go Up