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
Member
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   
GeneralConverted C#memberMetaGP26 Mar '13 - 5:55 
This is an interesting solution. The only feedback I have at this point results from the fact that it is converted from C# which forces the emulation of C# "like" commands in the resulting Java. It appears you are a C# programmer attempting to help the Java community, which is great.
 
I am inclined to take the converted code and build a true Java solution from it. It also needs to be packaged into a jar file for general usage in a Java project. But it's not really built to support that. Again a result of starting as a C# application.
 
Thank you for your efforts. It will be a big help in the GIS project I have ahead of me.
Questionfaster alternativesmemberRadu Motisan25 Feb '13 - 9:05 
Great code, however due to its complexity it is also more resource consuming.
 
A simpler approach can be seen here: https://code.google.com/p/avr-nmea-gps-library/[^]
 
It is a simple , microcontroller optimized NMEA parser that can be customized for more sentences if needed.
GeneralRe: faster alternativesmemberMetaGP26 Mar '13 - 5:46 
You refer to an embedded hardware solution. Many of us are in need of an API for our own custom programming. Are you the developer of the embedded application? Please try not to inadvertently misdirect people in the attempt at making your application more visible. Cool | :cool:
QuestionMy Vote of 5memberclwong8819 Feb '13 - 19:07 
Good Job.
QuestionI can't find the method declared as "string BuildSentence(NMEAStandartSentence)"memberqiaoyun28 Nov '12 - 19:17 
Thank you much for your excellent work. I'm most appreciative of your kindness of sharing it.
 
But when I tried to build a sentence by using something like the following.
 
NMEAStandartSentence standard = new NMEAStandartSentence();
string newStandartSentence = NMEAParser.BuildSentence(standard);
 
I can't find the method declared as "string BuildSentence(NMEAStandartSentence)" but another one which is declared as "string BuildSentence(TalkerIdentifiers talkerID, SentenceIdentifiers sentenceID, object[] parameters)".
 
So I am wondering which is the newer one. and should I just use the latter?
AnswerRe: I can't find the method declared as "string BuildSentence(NMEAStandartSentence)"membercarpintero483 Dec '12 - 6:40 
Hi, there is no way to build sentence from empty object first, as you trying it - NMEAParser unable to infer TalkerID, SentenceID and parameters list from empty object Smile | :)
 
And i suppose, since TalkerID and SentenceID are enums (always have determined values), it will be safe to add functionality as you wish. In near future.
 
Thank you for your message!
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?membercarpintero4819 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?memberRomanRdgz20 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?membercarpintero4821 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
Web03 | 2.6.130523.1 | Last Updated 8 Jan 2013
Article Copyright 2011 by carpintero48
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid