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?
QuestionExcellent - just what I've been looking for for agesmemberGlen Wardrop31 Jul '12 - 3:08 
spelling remains a bit quirky but that's no problem
I am updating a number of the sentence profiles to meet my requirements (sailing performance monitoring ) - is there a mechanism for me to pass on the changes and reasons ?
GeneralRe: Excellent - just what I've been looking for for agesmembercarpintero483 Aug '12 - 18:27 
I'm glad you find it usefull!
 
There are two ways to add some new sentences support:
1) if it's a proprietary sentences of known manufacturer, you can add templates in runtime, just like this:
 
NMEAParser.AddProprietarySentenceDescription(ManufacturerCodes.GRM, "<sent_name>", "<formatting string>");
 
2) if no such manufacturer in NMEAParser, or if you find incorrect/irrelevant sentence description, or you have description for new/unknown sentence - say it to me by email (dikarev-aleksandr[at]yandex.ru, and i'll update NMEAParser.
GeneralMy vote of 5membervital79231 May '12 - 18:05 
Good work. Useful!
GeneralRe: My vote of 5membercarpintero485 Jun '12 - 6:53 
You're welcome!
QuestionJava versionmemberbinarytoys28 May '12 - 23:41 
У вас нет желания сделать то-же самое но на Java и на коммерческой основе? Напишите - обсудим детали.
AnswerRe: Java versionmembercarpintero4830 May '12 - 8:08 
Многое из того что написано, основано на C#-ском syntax sugar. На Java это будет не так интересно делать, конечно мысли такие были, но это где-то может быть в 475-ую, или 896-ую очередь)). Так что на данный момент нет ни времени ни желания.
AnswerRe: Java versionmembercarpintero484 Jan '13 - 22:42 
Если ещё есть необходимость - библиотека переведена на Java. Частично при помощи Tangible Code Converter. Ссылка на скачивание вверху статьи. Код возможно несколько сыроват, буду признателен за любые замечания и сообщения об ошибках.
GeneralRe: Java versionmemberbinarytoys5 Jan '13 - 1:59 
Спасибо, необходимость по-прежнему существует, загружаю библиотеку и буду тестировать!
GeneralMy vote of 5memberAndrei Chernov3 Feb '12 - 22:35 
thank you!
GeneralRe: My vote of 5membercarpintero486 Feb '12 - 6:54 
You're welcome!
pojaluista, tovarish! )
QuestionExample code for building a sentencememberErrr71717 Dec '11 - 17:39 
First of all, let me tell you this is an incredible piece of work ... good design and the implementation is the best of all the projects in The Code Project website. You lapse into Russian every once in awhile in the comments, but that's okay. Smile | :) The code is easy enough to read. Thank you for sharing.
 
I'd like to see an example of the sentence creation. More specifically, I'm looking for an example on how to create a TTM sentence. Regards, Ed.
AnswerRe: Example code for building a sentencemembercarpintero4818 Dec '11 - 18:45 
Thanks for your message!
I have updated NMEAParser (TTM message format was obsolete).
 
You can build TTM sentence as follows:
 
object[] ttmParameters = new object[] 
            { 
                0,             // Target number, (00 - 99)
                1000.0,        // Target distance from own ship
                10.0,          // Bearing from own ship
                "True",        // Target bearing is T / R (True / Relative)
                1.0,           // Target Speed
                10.0,          // Target Course
                "True",        // Target course is T / R (True / Relative)
                10.0,          // Distance of Closest Point of Approach (CPA)
                0.0,           // Time to CPA in minutes, positive is approaching target, ( - ) negative is moving away.
                "Knots",       // Speed / Distance units K / N / S (Km / Knots / Statute miles)
                "Target name", // User data - generally target name
                "Tracking",    // Target Status L / Q / T (Lost from tracking process / Query - in process of acquisition / Tracking at the present time)
                "R",           // Reference target = R, null otherwise
                DateTime.Now,  // Time of data in UTC format (hhmmss.ss)
                "Automatic",   // Type of target acquisition A / M (Automatic / Manual)
            };
 
            // Build sentence
            var sentenceStr = NMEAParser.BuildSentence(TalkerIdentifiers.GP, SentenceIdentifiers.TTM, ttmParameters);
 
            // And parse it to check
            var parsedSentence = NMEAParser.Parse(sentenceStr);

QuestionMissing NMEA.csproj from zip filememberDorj816 Dec '11 - 0:47 
I am interested in the tester. The NMEA.csproj component is mssing from NMEAtester.zip. Could this be added so the program can be run?
Peter Felgate

AnswerRe: Missing NMEA.csproj from zip filemembercarpintero4817 Dec '11 - 5:16 
Thanks for message. Fixed.
QuestionNMEA 2.30/3.01?memberrubendepedro17 Nov '11 - 4:15 
Hi
 
Congrats for your code!
 
I made a few tests with a simulator called NmeaTalker and the only version the parser supported was 2.20. There is or will be a way to support the version 2.30/3.01?
 
Regards,
Ruben P.

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

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