Prefix
No matter how trite it may sound, but after searching for a ready-made solution that could (in my mind) fully support the work with
NMEA messages, I did not find it.
Having studied the
official document , I was completely imbued with the idea to implement it without fail, and without thinking twice he took himself on a show.
Plot
Forgive me, people are knowledgeable, but for other clarity, I still briefly describe the physics of the phenomenon.
So, the message of the NMEA standard, in the standard itself is called the "sentence", the one who these "sentences" "says" - "Talker". For example, GPS-applied within NMEA has the identifier "GP", and our
answer to the Chamberlain is "GL".
')
Existing solutions either worked with only these two types of devices, and at best understood various vendor-specific (Germin, UBLox, etc.) command receivers.
And who knows, all of a sudden it will be urgently necessary to interpret data coming from an atomic clock (Talker: ZA), or positioned on the Loran-C system (Talker: LC), but the opportunity to chat with the autopilot (Talker: AG) cannot be ruled out at all!
In essence, the standard describes 34 types of devices acting as talkers, about 74 types of messages (sentence), the possibility of using a proprietary code (Talker: P) - its command set at the discretion of the device manufacturer. And it would be just a shame to take and not cover the whole kitchen under a single namespace!
In general, I was repelled by the desired result. As a result, the task arose so that the following code worked:
NMEASentence parsedSentence = NMEAParser.Parse(sourceString);
and further:
string NMEASentenceString = NMEAParser.Build(parsedSentence);
And so that NMEASentence looks like this from the inside:
public sealed class NMEASentence { public TalkerIdentifiers TalkerID; public SentenceIdentifiers SentenceID; public object[] parameters; }
By the way, the general view of the NMEA sentence is:
$ <talker ID> <entence ID,> [parameter 1], [parameter 2], ... [<* checksum>] <CR> <LF>This is an ASCII string no longer than 82 characters, with a "$" at the beginning and at the end. There is no difficulty in parsing such a string - everything is done by simple string.Split () ;, the problem is how to check and parse specific expressions.
The standard describes several basic data types, in the standard documentation they are indicated as follows:
"X" - whole
"Xx" - real
"C - c" - string
"Hh" - hexadecimal
"Llll.ll" - latitude
"Yyyyy.yy" - longitude
"Hhmmss.ss" - time
"Ddmmyy" - date
"A" is a symbol
there is still a magic string "...", which means that all subsequent message parameters are of the same type as the previous one.
To implement parsing, various options have been thought out, ranging from examining each command from something basic and implementing the logic of each command separately to heuristically parsing each parameter.
The decision turned out to be on the surface: apply the
Magic strings anti-pattern !
Since all documents describe the format of messages in this form:
$ GPRMA, a, xx, xx, xx, xx, xy, xy, xy, xy, xy, xy, xyso let them be stored in the code - at the same time adding support for new messages is extremely simple:
1) copy the description from the manual
2) insert into the code
3) ???
4) PROFIT
Under the three questions the next idea.
All message descriptions are stored in the dictionary, where the key is the message identifier, enum:
public enum SentencesIdentifiers { AAM, ALM, APA, ... }
and the value sought "magic line" from the manual. It looks like this:
public static Dictionary<SentenceIdentifiers, string> SentencesFormats = new Dictionary<SentenceIdentifiers, string>() { { SentenceIdentifiers.AAM, "A,A,xx,N,c--c" }, { SentenceIdentifiers.ALM, "xx,xx,xx,xx,hh,hhhh,hh,hhhh,hhhhhh,hhhhhh,hhhhhh,hhhhhh,hhh,hhh" }, { SentenceIdentifiers.APB, "A,A,xx,a,N,A,A,xx,a,c--c,xx,a,xx,a" }, ...
What to do with it now?
The output is as follows: selecting the message parameters and its identifier, determine its format description from the
SentencesFormats dictionary, which can then be broken down into components, and then fed to the input of some method:
private static object ParseToken(string token, string formatter) { ??? }
The most awkward part of the whole system, according to all laws and alphabets, will settle under these three questions, in the form of some long switch:
switch (formatter) { case "xx": { return double.Parse(token, CultureInfo.Invariant); } ... default: return token;
This bottleneck would lose all the flexibility of the system, which is not good, so it’s best to do so:
return parsers[format](token);
Under the name of
parsers and hides just the most sugar:
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) }, ... { "xx", 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) }, ... };
As you can see, this is the dictionary, where the key is the formatting string, and the value is the function that turns the parameter string into an object.
Such a dictionary can be serialized and easily expanded with new entries, as well as a list of supported messages.
Similarly, with proprietary messages, with minor deviations from the above scenario.
Postfix
The ideas described above were implemented in full in the NMEAParser library.
There is also a complete list of manufacturer codes, a list of 222 reference points (Datums, DOP).
I would like to attach the archive with the library sources to the topic, but since there is no such possibility, I’m sorry to link to my own
article on CodeProject, not for my own PR, for the sake of people trying to make it work for free.
I will be glad to hear criticism, suggestions and suggestions.
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~
Update: Java port appeared in the specified link to CodeProject