Click here to Skip to main content
Click here to Skip to main content

NMEA 0183 sentence parser/builder

, 10 Dec 2013
Rate this:
Please Sign up or sign in to vote.
Library for working with NMEA0183 devices

Introduction

NMEA 0183 is a combined electrical and data specification for communication between marine electronic devices such as echo sounder, sonars, anemometer, gyrocompass, autopilot, GPS receivers, and many other types of instruments.

NMEA 0183 standard uses a text-based (ASCII) serial communications protocol. It defines the rules for transmitting "sentences" from one "talker" to multiple listeners.

Yes, there are many different NMEA sentence parser implementations in C#. But I have not found any implementation with a full list of talker IDs and sentence IDs. So, this is my attempt to make it.

Shortly about NMEA 0183 protocols

There are two layers in NMEA 0183:

  • data link layer
  • application layer protocol

In fact, data link layer defines only serial configuration:

  • bit rate (typically 4800)
  • 8 data bits
  • no parity checking
  • 1 stop bit
  • no handshake

Application layer is more complex, but not so. A common NMEA sentence format is listed below:

$<talker ID><sentence ID,>[parameter 1],[parameter 2],...[<*checksum>]<CR><LF>

It is necessary to specify:

NMEA defines two types of sentences: proprietary and non-proprietary. Non-proprietary sentences have a one of standard two-letter talker ID (e.g., GP for GPS unit, GL for glonass unit, etc.), and one of standard three-letter sentence ID (e.g., GLL for geographic location GPS data, DBK - depth below keel, and so on). All of these talker IDs and sentence IDs can be found in the official paper. Non-proprietary sentences have a 'P' letter instead of the standard talker ID, followed by a three-letter standard manufacturer code (GRM for Garmin, MTK for MTK, etc.), and further follows any string - name of proprietary command (depends on the specific manufacturer). Maximum length for sentences – 82 characters.

I will explain it with an example for a (standard) 'GLL' GPS sentence:

GLL - means geographic location

$GPGLL,1111.11,a,yyyyy.yy,a,hhmmss.ss, A*hh <CR><LF>

Parameters list description:

  1. llll.ll - latitude
  2. 'N' letter for North, 'S' - for south
  3. yyyyy.yy - longitude
  4. 'E' letter - for east, 'W' - for west
  5. UTC time in moment of measurement
  6. 'A' letter - data valid, 'V' - data not valid
  7. checksum

Example: $GPGLL,5532.8492,N,03729.0987,E,004241.469,A*33

and proprietary Garmin 'E' sentence:

PGRME - means Estimated Error Information
$PGRME,x.x,M,x.x,M,x.x,M*hh <CR><LF>

Parameters list description:

  1. x.x - Estimated horizontal position error (HPE) 0.0 to 999.9 meters
  2. M - means meters
  3. x.x - Estimated vertical error (VPE) 0.0 to 999.9 meters
  4. M - means meters
  5. x.x - Estimated position error (EPE) 0.0 to 999.9 meters
  6. checksum

Problem and Solution

The problem consists of parsing any possible NMEA 0183 sentence. The solution is listed below:

At first we will define the enums for the talker IDs, standard sentence IDs, and standard manufacturer codes, as follows (full enum definitions not present for space, see code for more info):

public enum TalkerIdentifiers
{
    AG,
    AP,
    CD,
    CR,
    CS,
    CT,
    . . .
}

public enum SentenceIdentifiers
{
    AAM,
    ALM,
    APA,
    APB,
    ASD,
    BEC,
    BOD,
    BWC,
    . . .
}

public enum ManufacturerCodes
{
    AAR,
    ACE,
    ACR,
    ACS,
    ACT,
    AGI,
    AHA,
    AIP,
    . . .
}

To implement parsing for all supported data types, I decided to use a sentence-specific formatters dictionary. Below you can see the main idea, a part of the code:

private static Dictionary<SentenceIdentifiers,string> SentencesFormats =
            new Dictionary<SentenceIdentifiers,string>() { { SentenceIdentifiers.AAM, 
                "A=Arrival circled entered,A=Perpendicular passed at way point,x.x,N=nm|K=km,c--c" },
                 { SentenceIdentifiers.ALM, "x.x,x.x,xx,x.x,hh,hhhh,hh,hhhh,hhhhhh,hhhhhh,hhhhhh,hhhhhh,hhh,hhh" },
                 . . .
                 { SentenceIdentifiers.BOD, "x.x,T=True|M=Magnetic,x.x,T=True|M=Magnetic,c--c,c--c" },
                 . . .
               };

And custom methods for parsing specific formatter related data fields can be stored as follows:

private static Dictionary<string,Func<string,object>> parsers = 
           new Dictionary<string,Func<string,object>>()
{
    { "x", x => int.Parse(x) },
    { "xx", x => int.Parse(x) },
    { "xxx", x => int.Parse(x) },
    { "xxxx", x => int.Parse(x) },
    { "xxxxx", x => int.Parse(x) },
    { "xxxxxx", x => int.Parse(x) },
    { "hh", x => Convert.ToByte(x, 16) },
    { "hhhh", x => Convert.ToUInt16(x, 16) },
    { "hhhhhh", x => Convert.ToUInt32(x, 16) },
    { "hhhhhhhh", x => Convert.ToUInt32(x, 16) },
    { "h--h", x => ParseByteArray(x) },
    { "x.x", x => double.Parse(x, CultureInfo.InvariantCulture) },
    { "c--c", x => x },
    { "llll.ll", x => ParseLatitude(x) },
    { "yyyyy.yy", x => ParseLongitude(x) },
    { "hhmmss", x => ParseCommonTime(x) },
    { "hhmmss.ss", x => ParseCommonTime(x) },
    { "ddmmyy", x => ParseCommonDate(x) },
    { "dddmm.mmm", x => ParseCommonDegrees(x) }
};

You see that the key in this dictionary – formatting string, and value – Func<string, object>, is initialized as a lambda expression for compactness. And now we can create a method for parsing a list of data fields, obtained from the NMEA sentence:

private static object[] ParseParameters(List<string> parameters, string formatString)
{
    var formatTokens = formatString.Split(new char[] { ',' });

    if (formatTokens.Length == parameters.Count)
    {
        List<object> results = new List<object>();

        for (int i = 0; i < parameters.Count; i++)
        {
            results.Add(ParseToken(parameters[i], formatTokens[i]));
        }

        return results.ToArray();
    }
    else
    {
        throw new ArgumentException("Specified parameters and format string has different lengths");
    }
}


private static object ParseToken(string token, string format)
{
    if (string.IsNullOrEmpty(token))
        return null;

    if (format.Contains(formatEnumPairDelimiter))
    {
        var items = format.Split(formatEnumDelimiters);
        Dictionary<string,string> enumDictionary = new Dictionary<string,string>();

        for (int i = 0; i < items.Length; i++)
        {
            var pair = items[i].Split(formatEnumPairDelimiters);
            if (pair.Length == 2)
            {
                enumDictionary.Add(pair[0], pair[1]);
            }
            else
            {
                throw new ArgumentException(string.Format("Error in format token \"{0}\"", format));
            }
        }

        if (enumDictionary.ContainsKey(token))
        {
            return enumDictionary[token];
        }
        else
        {
            return string.Format("\"{0}\"", token);
        }
    }
    else
    {
        if (format.StartsWith(arrayOpenBracket) && token.EndsWith(arrayCloseBracket))
        {
            return ParseArray(token, format.Trim(arrayBrackets));
        }
        else
        {
            if (parsers.ContainsKey(format))
            {
                return parsers[format](token);
            }
            else
            {
                return string.Format("\"{0}\"", token);
            }
        }
    }
}

And at last, the main public method for parsing an NMEA sentence:

private static NMEASentence ParseSentence(string source)
{
    var splits = source.Split(FieldDelimiter.ToString().ToCharArray());
    List<string> parameters = new List<string>();

    if (splits.Length > 1)
    {
        var sentenceDescription = splits[0];
        if (sentenceDescription.Length >= 4)
        {
            string talkerIDString;
            string sentenceIDString;
            
            if (sentenceDescription.StartsWith(TalkerIdentifiers.P.ToString()))
            {
                // Proprietary code
                if (sentenceDescription.Length > 4)
                {
                    var manufacturerIDString = sentenceDescription.Substring(1, 3);
                    sentenceIDString = sentenceDescription.Substring(4);

                    for (int i = 1; i < splits.Length; i++)
                    {
                        parameters.Add(splits[i]);
                    }

                    return ParseProprietary(manufacturerIDString, sentenceIDString, parameters);
                }
                else
                {
                    throw new ArgumentException(string.Format("Empty Sentence ID " + 
                      "in proprietary Sentence \"{0}\"", sentenceDescription));
                }
            }
            else
            {
                // Not a proprietary code
                TalkerIdentifiers talkerID = TalkerIdentifiers.unknown;
                talkerIDString = sentenceDescription.Substring(0, 2);
                sentenceIDString = sentenceDescription.Substring(2, 3);

                try
                {
                    talkerID = (TalkerIdentifiers)Enum.Parse(typeof(TalkerIdentifiers), talkerIDString);
                }
                catch
                {
                    throw new ArgumentException(string.Format(
                      "Undefined takler ID \"{0}\"", talkerIDString));
                }

                for (int i = 1; i < splits.Length; i++)
                {
                    parameters.Add(splits[i]);
                }

                return ParseSentence(talkerID, sentenceIDString, parameters);
            }                                        
        }
        else
        {
            throw new ArgumentException(string.Format("Wrong sentence " + 
              "description: \"{0}\"", sentenceDescription));
        }
    }
    else
    {
        throw new ArgumentException(string.Format(
          "No field delimiters in specified sentence \"{0}\"", source));
    }            
}

May be it is not the best way for implementation, but I use container classes for standard and proprietary sentences, returning in the Parse method. Both classes are inherited from the base class NMEASentence:

public abstract class NMEASentence
{
    public object[] parameters;
}

//‘NMEAStandartSentence’ class:

public sealed class NMEAStandartSentese : NMEASentence
{
    public TalkerIdentifiers TalkerID { get; set; }
    public SentenceIdentifiers SentenceID { get; set; }        
}

//And ‘NMEAProprietarySentence’ class:

public sealed class NMEAProprietarySentese : NMEASentence
{
    public string SenteseIDString { get; set; }
    public ManufacturerCodes Manufacturer { get; set; }
}

Using the Code

Notice, if you use a .NET Framework version lower than 3.5 you need to add a conditional compilation constant:

FRAMEWORK_LOWER_35

NMEAParser also can build NMEA0183 sentences. There are two methods for building sentences: BuildSentence and BuildProprietarySentence. You can use these methods with an instance of the NMEAStandartSentence or NMEAProprietarySentence class as a parameter:

NMEAStandartSentence standard = new NMEAStandartSentence();
NMEAProprietarySentence proprietary = new NMEAProprietarySentence();

// setting up properties for 'standard' and 'proprietary' here

string newStandartSentence = NMEAParser.BuildSentence(standard);
string newProprietarySentence = NMEAParser.BuilProprietarySentence(proprietary);

And to parse lines that come from, e.g., your GPS unit:

// Waypoint arival alarm sentence (AAM), talker - GP (GPS), for example
string stringToParse = "$GPAAM,A,A,0.10,N,WPTNME*32\r\n";
                        
try
{
        // try to parse sentence
        var parsedSentence = NMEAParser.Parse(stringToParse);
    if (parsedSentence is NMEAStandartSentence)
    {
        NMEAStandartSentence sentence = (parsedSentence as NMEAStandartSentence);
                    
        if ((sentence.TalkerID == TalkerIdentifiers.GP) &&
            (sentence.SentenceID == SentenceIdentifiers.AAM))
        {
            Console.WriteLine("Waypoint arrival alarm");
            Console.WriteLine(string.Format("Waypoint name: {0}", sentence.parameters[4]));
            Console.WriteLine(string.Format("Circle radius: {0}, {1}", 
                   sentence.parameters[2], sentence.parameters[3]));                                                
        }
    }
    else
    {
        if (parsedSentence is NMEAProprietarySentence)
        {
            // use parsed proprietary sentence                        
        }                    
    }                
}
catch (Exception ex)
{
    Console.WriteLine(string.Format("Unable parse \"{0}\": {1}", 
                        stringToParse, ex.Message));
}

Extra Info

An extra info: You can see in the ParseToken method that I add some extra functionality, especially there is a new data type – arrays; For formatters for arrays, use the following syntax: “[<standard>]”, values must be separated by ‘|’. Also I added ‘byte arrays’ with formatting string “h—h”. A byte array data field must be like this: “0x4d5a0023…”, with hexadecimal values of bytes.

Points of Interest

  • Did you learn anything interesting/fun/annoying while writing the code?

Yes, I did! Interesting - it is the first time I used lambdas. You can see how, in the code block:

{ "x.x", x => double.Parse(x, CultureInfo.InvariantCulture) },

Annoying - typing all talker IDs, sentence IDs, formatting strings, and manufacturer codes.

If you...

If you need some proprietary sentences support that is not supported yet - tell me, I'll try to add it. If you have found out a bug/slip/grammatical error, please let me know - I'll fix it as fast as I can.

Supported Now

Known talkers:
AG, AP, CD, CR, CS, CT, CV, CX, DE, DF, EC, EP, ER, GL, GP, HC, HE, HN, II, IN, LA, LC, OM, P, RA, SD, SN, TR, SS, TI, VD, DM, VW, WI, YX, ZA, ZC, ZQ, ZV
Supported standard sentences
AAM, ALM, APA, APB, ASD, BEC, BOD, BWC, BWR, BWW, DBK, DBS, DBT, DCN, DPT, DSC, DSE, DSI, DSR, DTM, FSI, GBS, GGA, GLC, GLL, GRS, GST, GSA, GSV, GTD, GXA, HDG, HDM, HDT, HSC, LCD, MSK, MSS, MWD, MTW, MWV, OLN, OSD, ROO, RMA, RMB, RMC, ROT, RPM, RSA, RSD, RTE, SFI, STN, TLL, TRF, TTM, VBW, VDR, VHW, VLW, VPW, VTG, VWR, WCV, WDC, WDR, WNC, WPL, XDR, XTE, XTR, ZDA, ZDL, ZFO, ZTG 
Supported proprietary sentences

GGarmin Corp : B, E, F, M, T, V, Z, C, CE, C1, C1E, I, IE, O
Martech Inc. : 001, 101, 102, 103, 104, 251, 300, 301, 313, 314, 320, 390, 420, 490, 520, 590, 605, 705
Trimble Navigation : DG, EV, GGK, ID, SM
Magellan : CMD, CSM, DRT, DWP, RTE, TRK, VER, WPL, ST
Motorola : G
Rockwell Int. : RID, ILOG
Starlink : B

SiRF (GlobalSat receivers): 100, 101, 102, 103, 104, 105  

History

  • 08/12/2013: Added SiRF proprietary sentences, added GNSSView demo application, some bugs fixed
  • 05/01/2013: Added Java version (partially automatic C#->Java code conversion)
  • 18/11/2011: Bugs fixed, added test application
  • 13/11/2011: Fixed many bugs, 4 new p-sentences.
  • 11/11/2011: Fixed some bugs in formatters, added ",..." formatter.
  • Trimble Navigation, Magellan, proprietary sentences.
  • Full data list (222 data ready, will uploaded soon)
  • 9 Nov 2011. Added Martech Inc. proprietary sentences support
  • Just a first version, all standard NMEA0183 sentences, Garmin proprietary sentences supported

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

carpintero48
Software Developer (Senior) JSC "SHTIL"
Russian Federation Russian Federation
underwater acoustics, communication and positioning.
Embedded software development. Digital signal processing.
 
Graduate of Volgograd Technical State University (2007),
Postgraduate student in Kovrov Technological State Academy (2012).
Follow on   LinkedIn

Comments and Discussions

 
GeneralMy vote of 5 PinmemberP1119r1m12-Nov-11 22:42 
GeneralRe: My vote of 5 Pinmembercarpintero4812-Nov-11 23:02 
QuestionMessage Removed Pinmember_beauw_11-Nov-11 12:01 
AnswerRe: Good design Pinmembercarpintero4811-Nov-11 19:26 
BugIrony PinmvpAspDotNetDev10-Nov-11 10:40 
GeneralRe: Irony Pinmembercarpintero4810-Nov-11 19:41 
BugRe: Irony PinmvpAspDotNetDev11-Nov-11 6:56 
GeneralRe: Irony Pinmembercarpintero4811-Nov-11 8:42 
Tnanks once again! I hope all occurrences of "sentense" fixed.
GeneralRe: Irony PinmemberSlacker00719-Dec-11 10:04 
GeneralRe: Irony PinprotectorAspDotNetDev19-Dec-11 10:31 
GeneralRe: Irony PinmemberSlacker00719-Dec-11 11:12 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140721.1 | Last Updated 10 Dec 2013
Article Copyright 2011 by carpintero48
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid