NMEA 0183 语句解析器/构建器






4.78/5 (25投票s)
用于处理 NMEA0183 设备的库。
引言
NMEA 0183 是一个结合了电气和数据规范的标准,用于海洋电子设备之间的通信,例如回声测深仪、声纳、风速计、陀螺罗盘、自动驾驶仪、GPS 接收器以及许多其他类型的仪器。
NMEA 0183 标准使用基于文本(ASCII)的串行通信协议。它定义了从一个“发送者”(talker)到多个“接收者”(listener)传输“语句”(sentence)的规则。
是的,C# 中有许多不同的 NMEA 语句解析器实现。但我没有找到一个包含所有发送者 ID 和语句 ID 的完整列表。所以,这是我的尝试。
简述 NMEA 0183 协议
NMEA 0183 分为两个层:
- 数据链路层
- 应用层协议
实际上,数据链路层只定义了串行配置。
- 比特率(通常为 4800)
- 8 数据位
- 无奇偶校验
- 1 停止位
- 无握手
应用层更复杂一些,但也不算太难。下面列出了常见的 NMEA 语句格式。
$<talker ID><sentence ID,>[parameter 1],[parameter 2],...[<*checksum>]<CR><LF>
需要指定
NMEA 定义了两种语句:专有语句和非专有语句。非专有语句具有标准的两位发送者 ID(例如,GP 代表 GPS 设备,GL 代表 GLONASS 设备等),以及标准的三个字符的语句 ID(例如,GLL 代表地理位置 GPS 数据,DBK 代表船底深度等)。所有这些发送者 ID 和语句 ID 都可以从官方文档中找到。专有语句在发送者 ID 的位置上有一个 'P' 字母,后面跟着一个标准的三个字符的制造商代码(GRM 代表 Garmin,MTK 代表 MTK 等),然后是任意字符串 - 专有命令的名称(取决于特定制造商)。语句的最大长度为 82 个字符。
我将用一个(标准)'GLL' GPS 语句的例子来解释。
GLL - 表示地理位置
$GPGLL,1111.11,a,yyyyy.yy,a,hhmmss.ss, A*hh <CR><LF>
参数列表描述
- llll.ll - 纬度
- 'N' 代表北,'S' 代表南
- yyyyy.yy - 经度
- 'E' 代表东,'W' 代表西
- 测量时的 UTC 时间
- 'A' 代表数据有效,'V' 代表数据无效
- 校验和
示例:$GPGLL,5532.8492,N,03729.0987,E,004241.469,A*33
以及 Garmin 的专有 'E' 语句。
PGRME - means Estimated Error Information $PGRME,x.x,M,x.x,M,x.x,M*hh <CR><LF>
参数列表描述
- x.x - 估计水平位置误差 (HPE) 0.0 到 999.9 米
- M - 表示米
- x.x - 估计垂直误差 (VPE) 0.0 到 999.9 米
- M - 表示米
- x.x - 估计位置误差 (EPE) 0.0 到 999.9 米
- 校验和
问题与解决方案
问题在于解析任何可能的 NMEA 0183 语句。解决方案如下。
首先,我们为发送者 ID、标准语句 ID 和标准制造商代码定义枚举,如下所示(为节省空间,未显示完整的枚举定义,请参阅代码获取更多信息)。
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,
. . .
}
为了实现对所有支持的数据类型的解析,我决定使用一个特定于语句的格式化器字典。下面是主要思路,部分代码。
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" },
. . .
};
特定于格式化器的数据字段的自定义方法可以如下存储。
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) }
};
您可以看到,这个字典中的键是格式化字符串,值是 Func<string, object>
,为了紧凑而初始化为 lambda 表达式。现在,我们可以创建一个方法来解析从 NMEA 语句中获得的字段列表。
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);
}
}
}
}
最后,用于解析 NMEA 语句的主要公共方法。
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));
}
}
也许这不是最佳的实现方式,但我使用容器类来存储标准和专有语句,在 Parse
方法中返回。这两个类都继承自基类 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
请注意,如果您使用的是低于 3.5 的 .NET Framework 版本,则需要添加条件编译常量。
FRAMEWORK_LOWER_35
NMEAParser
也可以构建 NMEA0183 语句。有两种构建语句的方法:BuildSentence
和 BuildProprietarySentence
。您可以使用 NMEAStandartSentence
或 NMEAProprietarySentence
类的实例作为参数来使用这些方法。
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);
还有,用于解析来自例如您的 GPS 单元的行。
// 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));
}
附加信息
附加信息:您可以在 ParseToken
方法中看到我添加了一些额外的功能,特别是有一个新的数据类型 - 数组;对于数组的格式化器,请使用以下语法:“[<standard>]”,值必须用‘|’分隔。我还添加了“字节数组”,格式化字符串为“h—h”。字节数组数据字段必须像这样:“0x4d5a0023…”,包含十六进制的字节值。
关注点
- 在编写代码时,您是否学到了什么有趣/好玩/令人恼火的东西?
是的,我学到了!有趣的是,这是我第一次使用 lambda 表达式。您可以在代码块中看到如何使用它。
{ "x.x", x => double.Parse(x, CultureInfo.InvariantCulture) },
令人恼火的是,需要输入所有发送者 ID、语句 ID、格式化字符串和制造商代码。
如果您...
如果您需要目前不支持的专有语句支持 - 告诉我,我会尝试添加。如果您发现了一个错误/疏漏/语法错误,请告诉我 - 我会尽快修复。
当前支持
已知发送者 |
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 |
支持的标准语句 |
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 |
支持的专有语句 |
Garmin 公司 : B, E, F, M, T, V, Z, C, CE, C1, C1E, I, IE, O SiRF (GlobalSat 接收器): 100, 101, 102, 103, 104, 105 |
历史
- 2013/08/12:添加了 SiRF 专有语句,添加了 GNSSView 演示应用程序,修复了一些错误。
- 2013/01/05:添加了 Java 版本(部分自动 C# -> Java 代码转换)。
- 2011/11/18:修复了错误,添加了测试应用程序。
- 2011/11/13:修复了许多错误,添加了 4 个新的 p-语句。
- 2011/11/11:修复了格式化器中的一些错误,添加了“...,”格式化器。
- Trimble Navigation、Magellan、专有语句。
- 完整数据列表(222 条数据已准备好,即将上传)。
- 2011 年 11 月 9 日。添加了 Martech 公司专有语句支持。
- 只是第一个版本,支持所有标准的 NMEA0183 语句和 Garmin 专有语句。