SlashDevin / NeoGPS

NMEA and ublox GPS parser for Arduino, configurable to use as few as 10 bytes of RAM
GNU General Public License v3.0
707 stars 195 forks source link

How should I handle parsing a proprietary NMEA sentence, in particular if it's the last sentence to be transmitted? #90

Closed hiltswaltts closed 6 years ago

hiltswaltts commented 6 years ago

Specifically I want to extract the GPS-UTC leap second offset from a Garmin $PGRMF sentence so that my project can accurately calculate the absolute number of elapsed seconds from a specific date/time, even after additional leap seconds are inserted.

Using Adafruit's GPS_HardwareSerial_EchoTest.ino, I've been able to confirm that, when it's enabled, $PGRMF is the last sentence to be transmitted my by my Garmin 18x LVC

I think I understand how to add leap seconds as a new fix member from the closed issue, "How to add data members to fix?," but I'm having trouble understanding exactly what I need to do to add a parser because the suggested examples in the documentation and all the relevant closed issues I could find have to do with with u-blox, which I'm not using, so I'm not clear on how relevant those are or are not to my situation.

So, the general questions I have at this point are:

SlashDevin commented 6 years ago

I want to extract the GPS-UTC leap second offset from a Garmin $PGRMF sentence

You're the first one to try this. Congrats?

Generally, you should follow the example of the ubloxNMEA class (in ubxNMEA.h & cpp). You need

bool GarminNMEA::parseField(char chr)
{
  if (nmeaMessage >= (nmea_msg_t) PGRM_FIRST_MSG) {

    switch (nmeaMessage) {

      case PGRMF: return parseF( chr );

      default: 
        break;
    }

  } else

    // Delegate
    return NMEAGPS::parseField(chr);

  return true;

} // parseField

Then declare and provide the parse function for the F sentence, using parseGGA in NMEAGPS.cpp as an example. Something like this:

bool GarminNMEA::parseF( char chr )
{
  #ifdef GARMINGPS_PARSE_F
    switch (fieldIndex) {
        case 3: return parseDDMMYY( chr );
        case 4: return parseTime( chr );
        case 5: return parseLeapSeconds( chr );  // <--  you will write this
        PARSE_LOC(6);
        //case 10: return parseFix( char ); // not needed, because next field sets status
        case 11: return parseFix( chr );
        //case 12: return parseSpeed( chr ); // a little messier because units are km/h, not kts/h
        case 13: return parseHeading( chr );
        case 14: return parsePDOP( chr );
        //case 15: return parseTDOP( chr ); // not yet supported
    }
  #endif

  return true;

} // parseF

Your GarminNMEA class will also declare and implement the parseLeapSeconds function, which should use one of the provided parseInt functions. I used parseSatellites from NMEAGPS.cpp as an example:

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

bool GarmingNMEA::parseLeapSeconds( char chr )
{
  static uint8_t newLeapSeconds; // just used for parsing

  if (parseInt( newLeapSeconds, chr )) {
    GPSTime::.leap_seconds = newLeapSeconds; // assign parsed value to global
  }

  return true;

} // parseGPSleapSeconds

Notice that this uses the leap_seconds variable declared in GPSTime.h. You will not have to add anything to gps_fix. When you read a fix, you can access GPSTime::leap_seconds:

  if (gps.available( gpsPort )) {
    fix = gps.read();
    Serial.print( F("GPS leap seconds = ") );
    Serial.println( GPSTime::leap_seconds );

One additional thing: you must also provide a msg_table function. This data structure identifies all the message types that can be parsed by your class. It can also be linked to other tables (e.g., the standard NMEA message table).

The ubloxNMEA class does not need that, because all the PUBX sentences are numbered: they are all $PUBX,##. There is no message type, like $PUBXFOO. The ublox message type is simply the numeric value of the first field.

Simply (!) declare your own static PROGMEM message table in your grmNMEA.cpp, with one entry for the "F" sentence. The types for this data structure are declared in NMEAGPSprivate.h. Using NMEAGPS.cpp as an example, you should have something like this

//----------------------------------------------------------------
// Garmin Proprietary NMEA Sentence strings (alphabetical)

#if defined(GARMINGPS_PARSE_F) | defined(NMEAGPS_RECOGNIZE_ALL)
  static const char garminF[] __PROGMEM =  "F";
#endif

static const char * const grm_nmea[] __PROGMEM =
  {
    #if defined(GARMINGPS_PARSE_F) | defined(NMEAGPS_RECOGNIZE_ALL)
      garminF,
    #endif
  };

const NMEAGPS::msg_table_t GarminNMEA::garmin_msg_table __PROGMEM =
  {
    GarminNMEA::PGRM_FIRST_MSG,
    (const msg_table_t *) NMEAGPS::nmea_msg_table,  //  <-- link to standard message table
    sizeof(garmin_nmea)/sizeof(garmin_nmea[0]),
    garmin_nmea
  };

Then your msg_table function in grmNMEA.h should look like this:

    NMEAGPS_VIRTUAL const msg_table_t *msg_table() const
      { return &garmin_msg_table; };

This will allow the NMEAGPS base class to detect the PGRMF sentence, in this way:

You should also have a grmNMEA_cfg.h file with a single configuration item. Using PUBX_cfg.h as an example,

#ifndef GRMNMEA_CFG_H
#define GRMNMEA_CFG_H

//------------------------------------------------------------
// Enable/disable the parsing of specific proprietary NMEA sentences.
//
// Configuring out a sentence prevents its fields from being parsed.
// However, the sentence type may still be recognized by /decode/ and 
// stored in member /nmeaMessage/.  No valid flags would be available.

//#define GARMINGPS_PARSE_F

#endif

Include this at the top of grmNMEA.h, just likeubxNMEA.h:

#ifndef _GRMNMEA_H_
#define _GRMNMEA_H_

//  Copyright (C) 2014-2017, SlashDevin
//
//  This file is part of NeoGPS
//
//  NeoGPS is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
//  NeoGPS is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with NeoGPS.  If not, see <http://www.gnu.org/licenses/>.

#include "NMEAGPS_cfg.h"

// Disable the entire file if derived types are not allowed.
#ifdef NMEAGPS_DERIVED_TYPES

#include "NMEAGPS.h"

#include "GRMNMEA_cfg.h"

#if !defined(NMEAGPS_PARSE_PROPRIETARY)
  #error NMEAGPS_PARSE_PROPRIETARY must be defined in NMEAGPS_cfg.h in order to parse PGRM messages!
#endif

#if !defined(NMEAGPS_PARSE_MFR_ID)
  #error NMEAGPS_PARSE_MFR_ID must be defined in NMEAGPS_cfg.h in order to parse PGRM messages!
#endif

//=============================================================
// NMEA 0183 Parser for Garmin GPS Modules.
//
// @section Limitations
// Very limited support for Garmin proprietary NMEA messages.
// Only NMEA messages of types F are parsed (i.e., $PGRMF).
//

class GarminNMEA : public NMEAGPS
{
    GarminNMEA( const GarminNMEA & );

public:

    GarminNMEA() {};

    /** Garmin proprietary NMEA message types. */
    enum grm_msg_t {
        PGRMF = NMEA_LAST_MSG+1,
        PGRM_END
    };
    static const nmea_msg_t PGRM_FIRST_MSG = (nmea_msg_t) PGRMF;
    static const nmea_msg_t PGRM_LAST_MSG  = (nmea_msg_t) (PGRM_END-1);

protected:
    bool parseMfrID( char chr )
        ...

In NMEAGPS_cfg.h, what do I need to set #define LAST_SENTENCE_IN_INTERVAL NMEAGPS:: to?

You should do this in NMEAGPS_cfg.h:

#define LAST_SENTENCE_IN_INTERVAL (NMEAGPS::nmea_msg_t)(NMEAGPS::NMEA_LAST_MSG+1)

// NOTE: For derived parser types, like Garmin, PUBX and UBX configs, use
//          (NMEAGPS::nmea_msg_t)(NMEAGPS::NMEA_LAST_MSG+1)

This will set the LAST_SENTENCE to the enum in your grmNMEA.h file.

Voilà!

Please feel free to ask for further clarification. I can use this to develop better documentatin for others trying to do the same thing. And if you're willing, I would be happy to add your new class to the distribution. _____ EDIT: 3 occurrences of PUBX changed to PGRM.

SlashDevin commented 6 years ago

Maybe this was more than you wanted to tackle? :)

I just added $PGRMF support for you. See example PGRM.ino

SlashDevin commented 6 years ago

@hiltswaltts, I do have two further questions. You said,

Specifically I want to extract the GPS-UTC leap second offset from a Garmin $PGRMF sentence so that my project can accurately calculate the absolute number of elapsed seconds from a specific date/time, even after additional leap seconds are inserted.

The date/time provided by all GPS receivers is already UTC. It is offset from the GPS time by the current number of GPS leap seconds. The NeoGPS fix.dateTime is a UTC date/time, so are you sure that you need the GPS leap seconds?

So far, I have only needed this when a GPS message contained a GPS date/time only (i.e. GPS week number + seconds/milliseconds since start of week), no UTC date/time. The $PGRMF message contains a UTC time, so you would not need leap seconds to offset fix.dateTime.

The only way to calculate an exact number of seconds between any two dates is to have a table that lists all these leap seconds and when they were implemented. Generally, you would need a table of all UTC discontinuities (e.g., including Gregorian/Julian changeover). Is that what you are doing?

Even then, will your program also detect new leap seconds as they are inserted? You would have to record the implementation timestamp in some non-volatile way.