65.9K
CodeProject 正在变化。 阅读更多。
Home

NMEA 0183 语句解析器/构建器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.78/5 (25投票s)

2011 年 11 月 9 日

CPOL

6分钟阅读

viewsIcon

264212

downloadIcon

15138

用于处理 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>

参数列表描述

  1. llll.ll - 纬度
  2. 'N' 代表北,'S' 代表南
  3. yyyyy.yy - 经度
  4. 'E' 代表东,'W' 代表西
  5. 测量时的 UTC 时间
  6. 'A' 代表数据有效,'V' 代表数据无效
  7. 校验和

示例:$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>

参数列表描述

  1. x.x - 估计水平位置误差 (HPE) 0.0 到 999.9 米
  2. M - 表示米
  3. x.x - 估计垂直误差 (VPE) 0.0 到 999.9 米
  4. M - 表示米
  5. x.x - 估计位置误差 (EPE) 0.0 到 999.9 米
  6. 校验和

问题与解决方案

问题在于解析任何可能的 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 语句。有两种构建语句的方法:BuildSentenceBuildProprietarySentence。您可以使用 NMEAStandartSentenceNMEAProprietarySentence 类的实例作为参数来使用这些方法。

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
Martech 公司 : 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

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 专有语句。
© . All rights reserved.