NeoGPS vs TinyGPS (not TinyGPS++)

Hello,

I have a Grove GPS connected to a nano, and using AltSoftSerial. The sketch I have uses TinyGPS to collect the NMEA data every 10 seconds, parses the lat, lon, and then sends this to a distance function that calculates the distance from the current position to a preset lat/lon.

Everything works fine, and the sketch (that does a lot more than the GPS part) currently consumes 58% of flash, and 70% of RAM.

Given all this, and the relative low frequency of gathering, parsing, and computing distance, what advantages will changing to NeoGPS give me compared to using TinyGPS? I've read the information available regarding being up to 80% faster in parsing, and a much lower memory footprint. However, I'm well within memory constraints, and I only need a lat/lon every 10 seconds or so.

Hoping /dev will see this and respond. I'm leaning towards changing to NeoGPS, and have the NMEA.ino sketch working, etc. Will post relevant parts of the sketch shortly.

Thanks in advance!

Here is all I'm doing with a GPS function that uses TinyGPS to grab the lat/lon every 6 seconds, and then passes this to a distance function. Everything works fine,

The two functions are as follows:

void gps_clue()
// This function reads NMEA sentences from the serial instance defined earlier
{
  start = millis();

  if (start - finish >= 6000) {
    finish = start;
    while (gpscom.available())     // While there is data on the RX pin...
    {
      int c = gpscom.read();    // load the data into a variable...
      // Serial.write(c);
      if (gps.encode(c))     // if there is a new valid sentence...
      {
        float latitude, longitude;
        gps.f_get_position(&latitude, &longitude);

        //Enter GPS lat/lon values for each clue here
        float f_lat, f_lon;
        f_lat = LAT_array[current_clue];
        f_lon = LON_array[current_clue];

        delay(500);
        distance = DistanceBetween2Points(latitude, longitude, f_lat, f_lon, KILOMETERS_PER_METER ); // call the distance function to figure out how far to the clue
        // Serial.print("Distance: "); Serial.println(distance); // debug in serial monitor if needed
      }
    }
  }
}

and...

float DistanceBetween2Points( float Lat1, float Lon1, float Lat2, float Lon2, float unit_conversion )
{
  float dLat = radians( Lat2 - Lat1 );
  float dLon = radians( Lon2 - Lon1 );

  float a = sin( dLat / 2.0f ) * sin( dLat / 2.0f ) +
            cos( radians( Lat1 ) ) * cos( radians( Lat2 ) ) *
            sin( dLon / 2.0f ) * sin( dLon / 2.0f );

  float d = 2.0f * atan2( sqrt( a ), sqrt( 1.0f - a ) );

  return d * EARTH_RADIUS_METERS * unit_conversion;
}

The NeoGPS site has quantitive numbers for speed, RAM footprint, and program size. I assume you have read those. Other unique attributes:

1) NeoGPS was designed to preserve the GPS solution that is being calculated by the GPS device. This means that NeoGPS tracks

* Validity - presence or absence of individual fields * Accuracy - no float variables, micros() timestamps, careful math calculations * Coherency - GPS update interval boundaries preserved (old data not mixed with new) * Configurability - Most applications do not use all sentences and all fields. Those characters can be quickly skipped, saving even more program space, RAM and execution time. The fix buffer size can also be adjusted for sketches that have latency (e.g., SD loggers).

2) The NeoGPS examples were designed to give beginners a good place to start. Many questions here are the result of modifying other libraries' example programs. They are "fragile" WRT to modifications, because they weren't written with that goal in mind.

Many other libraries' examples simply print too much. Because the print has to wait for the characters to go out, the input buffer gets filled and overflows. It may work at certain speeds or with certain devices, but not always.

The TinyGPS examples use a routine called smartDelay. It's not. This leads beginners down the path of a blocking program, keeping them from learning the "Blink Without Delay" technique. It gets more difficult to add new behavior, because the old behavior has delays that can't be avoided without restructuring the program. I see you are using delay in your code, so I'm not sure you'll understand this advantage.

The Adafruit_GPS examples use a TIMER (!) to read characters from the input buffer to a sentence buffer (a copy) that gets searched (string compares) for various terms before anything gets parsed, without verifying the checksum. When using a software serial port, this is asking for invalid data.

To avoid the need for a "smart" delay or for using a precious TIMER resource, NeoGPS examples show how to parse GPS data during the RX character interrupt.

3) Somewhat subjectively, the NeoGPS methods are designed to make the resulting sketch readable. IMO, this is the single most important quality of a sketch you are trying to understand. Here is your snippet, rewritten for NeoGPS, without delays:

NeoGPS::Location_t clues[ 10 ];
uint8_t updates = 0;

void gps_clue()
{
  while (gps.available( gpscom )) {
    gps_fix fix = gps.read();

    updates++;
    if ((updates >= 6) && fix.valid.location) {
      updates = 0;

      distance = fix.location.DistanceKm( clues[ current_clue ] );
      // Serial.print("Distance: "); Serial.println(distance); // debug in serial monitor if needed
    }
  }
}

Which one would you rather read? :)

So if you really don't care about any of those things, there are no advantages. If it ain't broke...

Cheers, /dev

Hello,

Thanks for responding! I think the example you gave says it all really. Ok, let me give this a go, and get NeoGps working. Yes, the sketch does work as is using tinygps, however the advantages of NeoGps are now clear.

N.

Hi dev,

How should I populate my arrays to use with NeoGPS?

Your example specifies:

NeoGPS::Location_t clues[ 10 ];

However, my arrays storing specific lat/lon are like this:

const float LAT_array[] = {0, 1.306282, 1.302307, 1.306282, 1.306586, 1.271420, 1.248074, 1.284255, 1.289428};
const float LON_array[] = {0, 103.832275, 103.836865, 103.832275, 103.832832, 103.819208, 103.821381, 103.860865, 103.846500};

Also, the NMEAdistance example bundled with your library notes the base coordinates as this:

NeoGPS::Location_t base( -253448688L, 1310324914L );

Thanks!

N.

my arrays storing specific lat/lon are like this:

Like this:

NeoGPS::Location_t clues[] =
  {
    { 1.306282, 103.832275 },
    { 1.302307, 103.836865 },
        ...

However…

Also, the NMEAdistance example bundled with your library notes the base coordinates as this:

    NeoGPS::Location_t base( -253448688L, 1310324914L );

On the Arduino, the float type only has 6 or 7 significant digits. It is not accurate enough to distinguish between 103.832275 and 103.832832. You can put the extra digits in your code, but it’s really using ~103.8322 and ~103.8238.

If you really have 9 significant digits, you have to use the integer forms of those lat/lon coordinates:

    { 12480740, 1038213810 },

You just multiple the degrees by 10,000,000. NeoGPS supports 10 significant digits, so I had to add a zero. You could provide another significant digit, if you can figure it out. NeoGPS is the only library that can use these accurate locations to calculate an accurate distance at small distances.

Here is a sketch with those coordinates:

#include <NMEAGPS.h>

NMEAGPS gps;

#define gpsPort Serial1 // BEST choice for a Mega, Leo or Due

// Next best, but requires specific pins (.g., 8&9 on an UNO):
// #include <AltSoftSerial.h>
// AltSoftSerial gpsPort;

// Third best, if specific pins can't be used:
// #include <NeoSWSerial.h>
// NeoSWSerial gpsPort( 3,4 ); // baud rate must be 9600, 19200 or 38400

// SoftwareSerial NOT RECOMMENDED

NeoGPS::Location_t clues[] =
  {
    { 13062820, 1038322750 },
    { 13023070, 1038368650 },
    { 13062820, 1038322750 },
    { 13065860, 1038328320 },
    { 12714200, 1038192080 },
    { 12480740, 1038213810 },
    { 12842550, 1038608650 },
    { 12894280, 1038465000 },
  };
uint8_t current_clue = 0;

uint8_t updates  = 0;
float   distance = 1000.0; // km

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

void loop()
{
  while (gps.available( gpsPort )) {
    gps_fix fix = gps.read();

    updates++;
    if ((updates >= 6) && fix.valid.location) {
      updates = 0;

      distance = fix.location.DistanceKm( clues[ current_clue ] );
      Serial.print( F("Distance: ") );
      Serial.println(distance);
    }
  }
}

Of course, I have no idea how the current_clue advances through the array. Perhaps something like this:

const uint8_t CLUE_MAX     = sizeof(clues)/sizeof(clues[0]);
const float   CLOSE_ENOUGH = 0.005; // 5m

    ...

void loop()
{
    ...

      distance = fix.location.DistanceKm( clues[ current_clue ] );
      Serial.print( F("Distance: ") );
      Serial.println(distance);

      // distance = 0.001; // to test advancing through the clues

      if (distance < CLOSE_ENOUGH) {

        if (current_clue < CLUE_MAX-1) {
          Serial.println( F("Next...") );
          current_clue++;
        } else {
          Serial.println( F("Here!") );
          for (;;); // hang here
        }

      }

Cheers,
/dev

Thanks for explaining and your help! What I love about how this works is how easy and clean it is to read through what is being done! You had mentioned this earlier.

I’m advancing through current_clue separately in another function, and passing the value to the gps function to read the right value from the array.

Will update shortly on how it goes… and likely will have a question or two on only parsing what I need, specifically lat/Lon to calculate distance, discarding everything else.

N.

Ok, all done. I’ve attached the sketch as well. Is there any further optimization that can be done to avoid parsing unnecessary components of the NMEA string?

As for the sketch, it’s a puzzle box that works as follows:

  1. 8 clues, each tied to a specific location.
  2. 8 RFID cards, each tied to a clue, and that can only be tapped once you reach the relevant location.
  3. Each RFID card has a hidden secret question printed somewhere on the card itself.
  4. 8 hangman style games that you can play once you tap the relevant RFID card, at the correct location.
  5. (This part not yet written)… some dotstar LEDs flash randomly as each clue is solved, and some stay on to indicate how far you are into the puzzle.
  6. A servo opens the box at the end.

N.

clue_box_with_hangman_arrays_neo.ino (14.7 KB)

Is there any further optimization that can be done to avoid parsing unnecessary components of the NMEA string?

You can comment out everything in GPSfix_cfg.h except GPS_FIX_LOCATION. That's the only piece you use.

There are several sentences that contain location. If NMEAorder said that your GPS device is emitting GLL sentences, I would choose that one. Comment out all the sentences except GLL, and set the LAST_SENTENCE to GLL:

   ...

//#define NMEAGPS_PARSE_GGA
#define NMEAGPS_PARSE_GLL
//#define NMEAGPS_PARSE_GSA
//#define NMEAGPS_PARSE_GSV
//#define NMEAGPS_PARSE_GST
//#define NMEAGPS_PARSE_RMC
//#define NMEAGPS_PARSE_VTG
//#define NMEAGPS_PARSE_ZDA

  ...

#define LAST_SENTENCE_IN_INTERVAL NMEAGPS::NMEA_GLL

If it doesn't send a GLL, use RMC instead.

In that same file, you could also comment out these:

    //#define NMEAGPS_STATS
    //#define NMEAGPS_COMMA_NEEDED
    //#define NMEAGPS_RECOGNIZE_ALL

I would suggest not using the String class. You are only using it for one variable, guessWord, and that one can easily be a const char *. Change these 3 lines:

String guessWord = String(10);

        wordSize = guessWord.length();

    guessWord.toCharArray(buf, wordSize + 1);

... to these:

const char* guessWord = nullptr;

        wordSize = strlen( guessWord );

    strcpy( buf, guessWord );

All other uses of guessWord do not have to change. That saves 1600 bytes of program space.

I think there may be an issue where you occasionally get old GPS information and calculate an incorrect distance. It's because you aren't constantly reading the GPS data; it's only read only during gps_clue. The next time you call gps_clue, it will read old GPS characters. After that, new GPS characters will get read and a new distance will be displayed (6 seconds later), so I'm not sure you've ever noticed it.

It's good that you are using AltSoftSerial, but I would also suggest using NeoSWSerial instead of SoftwareSerial. SoftwareSerial is very inefficient, because it disables interrupts for long periods of time. It will use about 95% of the CPU time, just waiting. This can interfere with other parts of your sketch or with other libraries. You may not notice it unless the RFID reader is sending data to the Arduino.

There are other things you could do to save RAM, but it sounds like there's enough room. If you are getting any display flicker, there are some structural changes you should do.

Cheers, /dev

Hi /dev,

Thanks again, will try your suggestions shortly, and post back. By old gps values, did you mean the following?

  1. A reading is taken, and distance is measured.
  2. I step forward to the next clue.
  3. The old distance is displayed, even though the new coordinates have changed.
  4. Six seconds (or less) later, a new distance is calculated from a new reading.
  5. The display is finally updated.

If so, yes. I handle this by resetting the distance to 150km, each time the clue number advances or retreats. This 150 is a special case, where I display "----" on screen. A new reading is taken, distance measured, and this is different than 150, so it will display the right distance, replacing the "----".

I picked 150 because I'm in Singapore, and it's a very small country, and 150km is further away than any of the clues will ever be from where I am.

As for further memory savings, I'm going to try storing the clue text arrays in flash, and then reading them into a temp buffer as needed. I don't quite know how to use PROGMEM yet, but will learn. Did you have any other tips to optimize SRAM usage?

I'm still quite new, and only started with Arduino sketches two months ago, with no prior programming experience. However, I want to learn the best ways to do things, hence using NeoGPS, AltSoftSerial, etc.

N.

I’ve attached the latest sketch, after moving the clues and hangman words to flash using PROGMEM, and incorporating your suggestion/code not to use String.

Next step is to stop using SoftwareSerial.

I also need to figure out how to reset the hangman game after all the lives are over. Right now, it subtracts all the lives correctly, and asks the player to “Try again”. However, it still remembers the correctly guessed letters from the last attempt, albeit doesn’t display them on screen. I need to reset the game to think that no letters have been guessed, but keep the same word. Will try and figure this out later tonight.

clue_box_with_hangman_arrays_neo_progmem.ino.ino (16.5 KB)

  1. Six seconds (or less) later, a new distance is calculated from a new reading.

That’s where it reads old GPS characters and (may) calculate an old distance. After 6 seconds, new characters have been used to calculate the new distance.

I think someone already suggested that you don’t need to copy from FLASH to print the clues:

      void *line1 = pgm_read_word( &string_table1[current_clue] );
      oled.print( (const __FlashStringHelper *) line1 );

That saves 50 bytes of RAM.

The int type is 2 bytes, and can represent numbers from -32768 to 32767. You can use smaller types for most of your variables, saving 1 bytes per variable:

uint_8 current_clue = 0; // up to 255 clues in one byte

bool left_last_button_state = false; // one byte

bool arrived = false;

Changing to bool makes your code much more readable. Some globals should be local:

// int gotOne = 0; no!

    ...
               guessWord = (const __FlashStringHelper *) pgm_read_word( &hangman_table[current_clue] );
               wordSize = strlen_P( (const char *) guessWord );

void hangman_select_button()
{
  bool gotOne = false; // yes!

  if (!alreadyGuessed) {
    alreadyGuessed = true;
    oled.setCursor(61, 0);
    oled.print("*");
    for (int i = 0; i < wordSize; i++) {
      if ( pgm_read_byte( &guessWord[i] ) == guessLetter) {
        oled.setCursor(i * 6, 2);
        oled.print(guessLetter);
        gotOne = true;
        totalRight++;
      }
    }

    // add letter to guessed letter array
    guessed[ guessedCount++ ] = guessLetter;

    // none of the letters match, draw the next body part on the hangman
    if (!gotOne) {

      hangman--;
      // draw_hangman(hangman);
      // draw_hangman();
    }
    else {
      // letter is in word, sound buzzer

    }

    //all letters have been guessed...WIN!
    if (totalRight == wordSize) {
      gameOver(1);
    }
  }
}

All the states could be packed into one byte like this

struct button_state_t {
  bool left  :1;
  bool right:1;
  bool left_last:1;
  bool right_last:1;
}
  __attribute__((packed)); // forces packing into *one* byte for all 4 bools

button_state_t button_state = { false, false, false, false };

void usageIs()
{
  if (button_state.left_last != button_state.left) {
     ...

That saves 3 more bytes for the bools. Not sure you need them…

Cheers,
/dev

/dev,

Thanks for this! Regarding presenting old GPS data, the distance displayed is always accurate once every 6 seconds. Doesn't seem to be an issue (yet). I know that changing clues within those six seconds will display the last clues' distance, so have accommodated for that. However, nothing else unusual occurs.

Also thanks for the suggestions on improving memory efficiency. I'll read up on the different variable types tomorrow, and modify the sketch accordingly.

I'll also try a few exercises with struct, get familiar with the concept first, and then modify the current sketch.

Finally I'm trying to figure out if I can use a compass sensor, along with gps data to present an arrow pointing towards the clue location. So, not just distance, but bearing/heading (kind of like how google maps or Apple maps does it).

It's been a wonderful learning experience so far, and I genuinely appreciate the help, and don't just blindly copy suggestions, without first reading in-depth, building my own small examples, etc.

N.