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

NMEA 0183 Sentence parser/builder

By , 7 Jan 2013
 

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 text-based (ASCII), serial communications protocol. It defines 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 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 more complex, but not so. Common NMEA sentence format listed below:

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

That else it is necessary to specify:

NMEA defines two types of sentences: proprietary and non-proprietary. Non-proprietary sentences has 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 this talker IDs and sentence IDs can be found in official paper. Non-proprietary sentences has a 'P' letter instead of standard talker ID, followed by three-letter standard manufacturer code (GRM for Garmin, MTK for MTK etc.), further follows any string - name of proprietary command (depends on specific manufacturer). Maximum length for sentences – 82 characters.

I will explain it on an example for (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

Problem consists in parsing any possible NMEA 0183 sentence. Solution listed below:

At first we will define enums for talker IDs, standard sentence IDs and standard manufacturer code as follows: (full enum's 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 decide to use sentence-specific formatters dictionary. Below you can see main idea, a part of 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 field 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 key in this dictionary – formatting string, and value – Func<string, object>, initialized as lambda expression for compactness. And now we can create method for parsing a list of data fields, obtained from 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 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 sentence, returning in ‘Parse’ method. Both classes inherited from 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 .NET framework version lower than 3.5 you need to add 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 this methods with instance of ‘NMEAStandartSentence’ or ‘NMEAProprietarySentence’ class as 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, came 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 ‘ParseToken’ method, that I add some extra functionality, especially, there are new ‘data type ‘ – arrays, formatters for array use following syntax: “[<standard>]”, values must be separated by ‘|’. Also I add ‘byte arrays’ with formatting string “h—h”, 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 for me i used lambdas. You can see how in code block with

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

annoying - typing ALL talkers IDs, sentence Ids, formatting strings and manufacturer codes.

If you...

IIf you need some proprietary sentences support, that is not supported yet - tell me, I'll try to add it first.

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
SSupported 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/td>
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

History

  • 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 JSC "SHTIL"
Russian Federation Russian Federation
Graduate of Volgograd Technical State University (2007),
2008 - 2012: Postgraduate student in Kovrov Technological State Academy.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionWhat if parsing binary messages?memberRomanRdgz18-Oct-12 23:55 
How would you change current code to be able to parse binary messages, without separators, just knowing what means each bit from each type of message?
 
I guess raw strings wouldn't help in that case...
AnswerRe: What if parsing binary messages? Pinmembercarpintero4819-Oct-12 19:48 
why do you need it? Result will be the same - you have parsed message, and don't care how parser did it.
Or you talking about another (not NMEA0183) standart?
GeneralRe: What if parsing binary messages? PinmemberRomanRdgz20-Oct-12 1:44 
That's it: not NMEA. There are many gps messages from commercialreceptor which are raw, and data comes binary. Also SBAS systems give its data in binary format.
 
I was looking for an elegant way of writing my code to get data from those messages, and found thia project. I really like the way you are paraing the NMEA messages, it's clear and smart. But in a gps raw message I don't have commas (colons?) to separate one data fiel from another, and since it is not a string I don't see how I could use raw strings to compare received message with them as you do.
 
So, any suggestion of how could I adapt your ideas to binary messages?
GeneralRe: What if parsing binary messages? Pinmembercarpintero4821-Oct-12 7:21 
Commas and colons - it doesn't matter, if you have field with fixed sizes and/or field separators - method will be similar. Raw strings - i don't know what you mean, in C# i use byte arrays for such purposes. Tell me which protocol support you need - may be it will be interesting to add such functionality to library.
 
PS Is it binary sirf or something like this?

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

Permalink | Advertise | Privacy | Mobile
Web04 | 2.6.130617.1 | Last Updated 8 Jan 2013
Article Copyright 2011 by carpintero48
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid