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   
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.
AnswerRe: NMEA 2.30/3.01?membercarpintero4817 Nov '11 - 5:35 
Thanks for your message! I haven't found a 2.30/3.01 specification, i have only version from wikipedia - link, if you know of freely available any NMEA specification - let me know, i'll update NMEAParser.
 
PS When i was working with my Quectel L10 GPS receiver, I found some inconsistencies and fix it (extra status field at the end of GGA, GSV and GLL sentences), but not uploaded it yet.
GeneralMy vote of 5memberP1119r1m12 Nov '11 - 22:42 
My vote of 5!
GeneralRe: My vote of 5membercarpintero4812 Nov '11 - 23:02 
Thanks a lot!
QuestionMessage Removedmember_beauw_11 Nov '11 - 12:01 
Message Removed
AnswerRe: Good designmembercarpintero4811 Nov '11 - 19:26 
Thank you for commenting! I tried to make it easily expanded and as easy as i can. And i think about sentences formats not as 'magic strings' anti-pattern but as database, integrated in code. These strings ('x.x' or 'hhmmss.ss' etc.) easy to understand for people who read manuals for GPS receivers.
BugIronymvpAspDotNetDev10 Nov '11 - 10:40 
It's a little ironic that an article about parsing sentences misspelled it as "sentense" in both the article title and throughout the code/article. Smile | :)
Somebody in an online forum wrote:
INTJs never really joke. They make a point. The joke is just a gift wrapper.

GeneralRe: Ironymembercarpintero4810 Nov '11 - 19:41 
thanks for message! Fixed.
BugRe: IronymvpAspDotNetDev11 Nov '11 - 6:56 
I noticed you updated the article/code to fix the "sentense" typo. Thumbs Up | :thumbsup: However, you still have a few instances of "sentense" in the code comments for "NMEAParser.cs".
Somebody in an online forum wrote:
INTJs never really joke. They make a point. The joke is just a gift wrapper.

GeneralRe: Ironymembercarpintero4811 Nov '11 - 8:42 
Tnanks once again! I hope all occurrences of "sentense" fixed.
GeneralRe: IronymemberSlacker00719 Dec '11 - 10:04 
It's a little ironic, that an article about parsing sentences, misspelled it as "sentense" in both the article title and throughout the code/article.
 
grammar helps, too. Wink | ;)

Just along for the ride.

"the meat from that butcher is just the dogs danglies, absolutely amazing cuts of beef." - DaveAuld (2011)
"No, that is just the earthly manifestation of the Great God Retardon." - Nagy Vilmos (2011)
"It is the celestial scrotum of good luck!" - Nagy Vilmos (2011)


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