In this post I'm gonna acquaint you with the NMEA format used in GPS receivers. Actually, you can read about that in details here, for example. In fact, it's a proprietary one but it's too old to not to be reverse-engineered :-)
We're gonna use just one type of NMEA sentence, namely GPRMC.
Since English description of that is available via the link above, I'll write it in terms of a regular expression (btw, I use Ruby 1.9.1, and AFAIK named groups are available from this version only).
%r{ \$GPRMC, (?<hh> \d{2}) (?<mm> \d{2}) (?<ss> [\d.]+), (?<valid> \w), (?<deg_latitude> \d{2}) (?<min_latitude> [\d.]+),N, (?<deg_longitude> \d{3}) (?<min_longitude> [\d.]+),E, (?<speed> [\d.]+), (?<angle> [\d.]+), (?<day> \d{2}) (?<month> \d{2}) (?<yy> \d{2}) }xIt's almost self-explanatory. Something has to be explained, though:
1) "x" after the last closing curly brace means "ignore all the whitespaces in my regular expression".
2) "N" and "E" are for "North" and "East". So you better take care of that if your location differs from that of St. Petersburg significantly :-)
3) Validness of the sentence is determined by one letter: 'A' for valid, 'V' for invalid (usually means absence of satellites' signal).
4) There're a few fields in the sentence after the date but they are hardly of our interest.
Now that we know the structure of a GPRMC sentence let's write a class in Ruby to deal with our data easily:
class GPRMC @@KNOT = 1.852 # for converting speed into km/hWe use this magic number 'cause the speed seen in a GPRMC sentence is measured in knots (well, if you feel familiar with knots, just replace the number with 1). @@ before the name of the variable means that it's a class variable, not an instance one (if so, it would be just the waste of memory).
Another class variable is our pattern:
@@pattern = %r{ \$GPRMC, (?<hh> \d{2}) ... # I suppose you know how to copy-paste }xNow let's write the constructor which will transform a GPRMC sentence (it's just a string) to the instance of the class:
def initialize(str) m = @@pattern.match(str) if not m then @valid = false else @valid = (m[:valid] == 'A') yy = m[:yy].to_i yyyy = yy < 83 ? 2000+yy : 1900+yy msec = (m[:ss].to_f - m[:ss].to_i) * 1000 @timestamp = Time.gm(yyyy, m[:month].to_i, m[:day].to_i, m[:hh].to_i, m[:mm].to_i, m[:ss].to_i, msec) @latitude = m[:deg_latitude].to_f + m[:min_latitude].to_f / 60.0 @longitude = m[:deg_longitude].to_f + m[:min_longitude].to_f / 60.0 @speed = @@KNOT * m[:speed].to_f @angle = m[:angle].to_f end endThe reason why yy is converted to yyyy in such a strange way is that it was 1983 when GPS was allowed for civilian use. All the instance variables aren't yet available for the outside world. We have to explicitly write getters to be able to read these variables:
attr_reader :valid, :timestamp, :latitude, :longitude, :speed, :angle end # end of class definitionThat's the power of metaprogramming: Ruby writes all the boilerplate code instead of you. By the way, Ruby has also attr_writer and attr_accessor functions for another two very common purposes (guess which?). And the last piece of code for today: getting an array of GPRMC class instances out of a NMEA logfile.
def readNMEA(filename) data = [] File.open(filename).each do |line| if line =~ /^\$GPRMC/ then tmp = GPRMC.new(line) if tmp.valid then data << tmp end end end return data endHere "line =~ /^\GPRMC" is just testing if line starts with "$GPRMC" by means of a simple regular expression. It should also be noted that after call of File.open the file will be closed automatically. But I'm not gonna explain all that stuff in details because my aim is to show how to deal with GPS data, not how to program in Ruby — enough books about that are published, so read them to gain a better understanding. For now, key topics are "blocks", "iterators" and "regular expressions".
Next post is about improving performance of all this
No comments:
Post a Comment