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 standart
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 standart talker ID, followed by three-letter standard manufacturer code (GRM for Garmin, MTK for MTK etc.), furher 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:
- llll.ll - latitude
- 'N' letter for North, 'S' - for south
- yyyyy.yy - longitude
- 'E' letter - for east, 'W' - for west
- UTC time in moment of measurement
- 'A' letter - data valid, 'V' - data not valid
- 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:
- x.x - Estimated horizontal position error (HPE) 0.0 to 999.9 meters
- M - means meters
- x.x - Estimated vertical error (VPE) 0.0 to 999.9 meters
- M - means meters
- x.x - Estimated position error (EPE) 0.0 to 999.9 meters
- checksum
Problem and Solution
Problem consists in parsing any possible NMEA 0183 sentence.
Solution listed below:
At first we will define enums for takler 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,
CV,
..">if (format.Contains(“=”))
{
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 {…}
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()))
{
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
{
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;
}
public sealed class NMEAStandartSentese : NMEASentence
{
public TalkerIdentifiers TalkerID { get; set; }
public SentenceIdentifiers SentenceID { get; set; }
}
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
NMEAParse 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();
string newStandartSentence = NMEAParser.BuildSentence(standard);
string newProprietarySentence = NMEAParser.BuilProprietarySentence(proprietary);
And to parse lines, came from, e.g. your GPS unit:
string stringToParse = "$GPAAM,A,A,0.10,N,WPTNME*32\r\n";
try
{
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)
{
}
}
}
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...
If 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 |
| Supported standart 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 |
Garmin 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
- 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 datum list (222 datums, 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