Префикс
Как бы банально это не звучало, но поискав готовое решение, которое могло бы (по моему разумению) полностью поддерживать работу с 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