Pull to refresh

Универсальный NMEA 0183 Parser/Formatter на C# (+ порт на JAVA)

Reading time4 min
Views13K

Префикс


Как бы банально это не звучало, но поискав готовое решение, которое могло бы (по моему разумению) полностью поддерживать работу с NMEA — сообщениями, я его не обнаружил.

Проштудировав официальный документ, я всецело пропитался идеей непременно это реализовать и недолго думая сам взял себя на понт.

Фабула


Да простят меня люди сведущие, но для прочей ясности я все же кратко опишу физику явления.
Итак, сообщение стандарта NMEA, в самом стандарте называются «sentence», тот, кто эти «предложения» «говорит» — «Talker». Так например, GPS-применик в рамках NMEA имеет идентификатор «GP», а наш ответ чемберлену — «GL».

Существующие решения работали либо только с этими двумя типами устройств, а в лучшем случае понимали различные специфические для конкретных производителей (Germin, UBLox, и пр.) приемников команды.
Да и кто знает, вдруг срочно нужно будет интерпритировать данные поступающие с атомных часов (Talker: ZA), или спозиционироваться по системе Loran-C (Talker: LC), ну а возможность поболтать с автопилотом (Talker: AG) вообще нельзя исключать!



По сути, стандарт описывает 34 типа устройств, выступающих в качестве talker-ов, около 74-х типов сообщений (sentence), возможность применения проприетарного кода (Talker: P) — своего набора команд на усмотрение производителя устройства. И было бы просто обидно взять и не охватить всю эту кухню под единый namespace!

Вообщем, я отталкивался от необходимого результата. В итоге, задача встала так, чтобы работал вот такой код:

NMEASentence parsedSentence = NMEAParser.Parse(sourceString);


и ещё:

string NMEASentenceString = NMEAParser.Build(parsedSentence);


И чтобы NMEASentence изнутри выглядел как-нибудь так:

public sealed class NMEASentence
{
   public   TalkerIdentifiers TalkerID;
   public   SentenceIdentifiers SentenceID;
   public   object[] parameters;
}


К слову сказать, общий вид NMEA sentence имеет такой:

$<talker ID><entence ID,>[parameter 1],[parameter 2],...[<*checksum>]<CR><LF>

Это ASCII-строка не длиннее 82 символов, со знаком "$" в начале и в конце. Разобрать такую строку на части сложности нет — все делается простым string.Split();, проблема состоит в том, как проверять и парсить конкретные выражения.
Стандарт описывает несколько основных типов данных, в документации по стандарту они обозначаются следующим образом:

«x» — целый
«x.x» — вещественный
«c--c» — строка
«hh» — шестнадцатеричный
«llll.ll» — широта
«yyyyy.yy» — долгота
«hhmmss.ss» — время
«ddmmyy» — дата
«a» — символ

ещё есть магическая строка "...", означающая, что все последующие параметры сообщения имеют такой же тип, как и предыдующий.

Для реализации парсинга продумывались различные варианты начиная от того, чтобы пронаследовать каждую команду от чего-то базового и реализовать логику каждой команды отдельно, до того, чтобы эвристически запарсить каждый параметр.

Решение же оказалось на поверхности: применим антипаттерн Magic strings!

Раз уж во всех документах описывают формат сообщений в таком виде:

$GPRMA,A,llll.ll,a,yyyyy.yy,a,x.x,x.x,x.x,x.x,x.x,a<CR><LF>

так пусть они в коде так и хранятся — заодно добавление поддержки новых сообщений черезвычайно просто:
1) копируем описание из мануала
2) вставляем в код
3) ???
4) PROFIT

Под тремя вопросами следующая идея.
Все описания сообщений хранятся в словаре, где ключ — идентификатор сообщения, enum:

public enum SentencesIdentifiers
{
   AAM,
   ALM,
   APA,
   ...
}


а значение, искомая «магическая строчка» из мануала. Выглядит это вот так:

public static Dictionary<SentenceIdentifiers, string> SentencesFormats =
            new Dictionary<SentenceIdentifiers, string>() { { SentenceIdentifiers.AAM, "A,A,x.x,N,c--c" },
                                                            { SentenceIdentifiers.ALM, "x.x,x.x,xx,x.x,hh,hhhh,hh,hhhh,hhhhhh,hhhhhh,hhhhhh,hhhhhh,hhh,hhh" },
                                                            { SentenceIdentifiers.APB, "A,A,x.x,a,N,A,A,x.x,a,c--c,x.x,a,x.x,a" },
            ...


Что теперь с этим делать?
Выход такой: выделив параметры сообщения и его идентификатор, определить его описание формата из словаря SentencesFormats, которое потом разбить на составляющие, а потом подавать на вход какого-нибудь метода:

private static object ParseToken(string token, string formatter)
{
     ???
}


Самая неуклюжая часть всей системы по всем законам и азбукам поселится под этими тремя вопросами, в виде какого-нибудь длинного switch:

switch (formatter)
{
   case "x.x": 
   {
       return double.Parse(token, CultureInfo.Invariant);
   }
   ...

   default:
      return token; // просто вернуть исходную строку, если непонятно, что с ней делать
}


На этом bottleneck терялась бы вся гибкость системы, что не есть хорошо, поэтому лучше всего это сделать так:

return parsers[format](token);


Под именем parsers и скрывается как раз самый сахар:

private static Dictionary<string, Func<string,object>> parsers = new Dictionary<string, Func<string,object>>()
        {
            { "x", x => int.Parse(x) },
            ...
            { "hh", x => Convert.ToByte(x, 16) },
            ...
            { "x.x", x => double.Parse(x, CultureInfo.InvariantCulture) },
            { "c--c", x => x },
            { "llll.ll", x => ParseLatitude(x) },
            { "yyyyy.yy", x => ParseLongitude(x) },
            { "hhmmss.ss", x => ParseCommonTime(x) },
            { "ddmmyy", x => ParseCommonDate(x) },
            ...
        };


Как видно это словарь, где ключ — форматирующая строка, а значение — функция, делающая превращающая строку-параметр в объект.

Такой словарь может быть сериализован и легко расширен новыми записями, как и список поддерживаемых сообщений.

Похожим образом дела обстоят с проприетарными сообщениями, с небольшими отклонениями от приведенного выше сценария.

Постфикс


Идеи, описанные выше были реализованы в полном объеме в библиотеке NMEAParser.
Там же есть и полный список кодов производителей, список из 222 точек привязки (Datums, DOP).

Мне хотелось бы прикрепить к топику архив с исходниками библиотеки, но поскольку такой возможности нет простите мне ссылку на собственную статью на CodeProject, не для собственного пиара ради — а для людей старался, чтоб труд даром не пропал.

Буду рад выслушать критику, пожелания и предложения.

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Обновление: в указанной ссылке на CodeProject появился порт на Java
Tags:
Hubs:
+9
Comments5

Articles