Saturday, November 13, 2010

GPS & Ruby: Introduction

This is the beginning of post series devoted to playing with data obtained by a GPS receiver. Here I assume that you are familiar with basic concepts of object-oriented programming and also assume that you don't know Ruby (honestly, neither do I, but that's gonna change eventually). So don't you worry, there will be a few comments about the code ;-)

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})
    }x
It'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/h
We 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
    }x
Now 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
    end
The 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 definition
That'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
end
Here "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 cr stuff.

No comments:

Post a Comment