Reading and Parsing GPS Sentences (a Linux Example)

This post shows how to read the GPS position from a GPS-enabled Multitech modem. This approach can be adapted to reading on other devices.

GPS Sentences

GPS devices communicate using “sentences”, which are single-line strings of comma-separated values. In the case of the Multitech modem, these sentences are streamed continuously from a COM port.

An excellent reference that summarizes GPS sentences can be found here. If you need to find more information, it’s best to search for "NMEA 0183", which is the specification that defines GPS communications.

The basic approach for getting a GPS position is:

  1. Open the port that’s emitting the GPS sentences.
  2. Read one sentence (meaning one line) at a time until you get a $GPGSA sentence, which indicates how reliable the reported position will be.
  3. Parse the $GPGSA sentence to see if you have a reliable position "fix".
  4. If the position will be reliable, read one sentence at a time until you get a sentence that reports the position. There’s more than one sentence type for this. I ended up using the $GPRMC sentence, which reports position as latitude and longitude. It will also report altitude (assuming you need the altitude and the modem has a reliable 3D "fix").
  5. Parse the $GPRMC sentence to get latitude, longitude, and, optionally, altitude.
  6.  
    The Multitech modem I worked with used the Linux gnu operating system, so the easy choice was to write a Linux script to retrieve the information, but the general approach will work with any language that can read from a COM port. It’s rare to find a language that can’t, though some languages that operate in a security "sandbox" (for example JavaScript) will prohibit interaction with a port.

    Reading from COM Ports in Linux

    In Linux the COM port is just another file. On the particular modem I was using, the GPS port was /dev/ttyS3. To see the GPS sentences from the command line, I just had to type cat /dev/ttyS3 and Linux would spew out the sentences until I stopped it.

    To read from the port in a program, just to redirect it to a file descriptor. In the example below I use file descriptor #5 to redirect the modem’s sentence-reporting port:

    # "Open" the port. Specifically, redirect port
    # /dev/ttyS3 to file descriptor 5
    exec 5</dev/ttyS3
    

     
    Jumping ahead a bit, it's good to "close" things when you're done. In this case, that means killing off the redirect:

    # "Close" the port. Specifically, stop redirecting 
    # the port to file descriptor 5
    exec 5<&-
    

     
    To read a line from the port, use the read command. This normally reads from user input into a variable, but in this case we're reading from the redirected COM port. I've also added a timeout of 10 seconds so the read can give up if the GPS port is unresponsive; that's the -t 10 below:

    # Read a line from file descriptor 5, which
    # is a redirect from the GPS port /dev/ttyS3
    read -t 10 RESPONSE <&5
    

     
    If the read command is successful, the next GPS sentence in the stream will be placed into the RESPONSE variable. If the command times out it will return 1, which you can check using the $? shell variable ($? holds the return value of the last executed command):

    read -t 10 RESPONSE <&5
    if [[ $? -eq 1 ]]
    then
      # close the port and return failure to the user
    fi
    

    Parsing the $GPGSA ("Fix" Information) Sentence

    A typical $GPGSA sentence looks like this:

    $GPGSA,A,3,04,05,,09,12,,,24,,,,,2.5,1.3,2.1*39

    In the comma-delimited list of values, the only ones I cared about were the first (which identifies the sentence) and the third (which indicates if the GPS reading will be reliable).

    The first field's value must of course be $GPGSA. The third field's value will be one of the following:

    • 1 means the GPS doesn't have a position fix.
    • 2 means the GPS has a 2-dimensional (latitude and longitude) fix. If you don't care about altitude (and I didn't), then a reliability of 2 is acceptable. If you do care about altitude you'll need a reliability of 3.
    • 3 means the GPS has a 3-dimensional (latitude, longitude, and altitude) fix. This value is also acceptable if you need only latitude and longitude because you can just ignore the altitude.

     
    Once you've determined that the position reading will be reliable, the next step is to find and parse a position sentence.

    Parsing the $GPRMC (Recommended Minimum GPS Data) Sentence

    A typical $GPRMC sentence looks like this:

    $GPRMC,123519,A,4807.038,N,01131.000,E,022.4,084.4,230394,003.1,W*6A
    

    In this comma-delimited list of values, the important fields are:

    1. When this field's value is $GPRMC, it indicates that the sentence has position information.
    2. This field will be A for active or V for void. A is good; V means there's an unexpected problem with the position reading. If you encounter a V you should start the whole position-reading process over again, including the $GPGSA reading. Another option is to quit and report failure.
    3. This is the absolute value of the latitude, but it needs some conversion. The number in the example is 4807.038, and it means 48 degrees and 7.038 minutes (48° 7.038"). A "minute" is a 60th of a degree. The next section shows how to convert from this format to a decimal degree value.
    4. This will be N if the latitude (field 4, above) is North, or S if the latitude is South. For many mapping applications, North is represented by a positive number and South by a negative number; for example latitude 45°S is specified as –45°.
    5. This is the absolute value of the longitude, using the same format as the latitude in field 4. The next section shows how to convert from this format to a decimal degree value.
    6. This will be E if the longitude is East, or W if the longitude is West. East is a positive longitude; West is negative.

    Converting the $GPRMC Latitude and Longitude Values

    Controls such as the Google Maps API want latitude and longitude values as degree values with decimal places rather than minutes, and South/West values as negatives. Here's how to convert them, using the value 4827.563 West:

    1. Divide the integer part of the latitude by 100. 4827 ÷ 100 = 48
    2. Get the "minutes" value by taking the remainder of the value divided by 100. 4827.563 - 100 × 48 = 27.563
    3. The result of step 2 is the "minutes" part of the degrees. Since there are 60 minutes per degree, divide the result from step 3 by 60 to get the decimal degrees. 27.563 ÷ 60 ≈ 0.45938
    4. Add the values from step 2 and 4. 48 + 0.45938 = 48.45938
    5. Make the result from step 5 negative if this is a latitude and it's South, or if it's a longitude and it's West. 48.45938 West = –45.45938

    Doing math in Linux scripting is more challenging than in most other languages, but it's certainly not impossible. I settled on using the bc utility, but you should use the math package you're most comfortable with. Here's a function to take a GPS value and direction and convert it to a signed decimal latitude/longitude:

    # Pass the GPS value and direction (N/S/E/W) to get the
    # decimal latitude/longitude.
    function gpsDecimal() {
        gpsVal=$1
        gpsDir ="$2"
        # Integer part of the lat/long
        gpsInt=`echo "scale=0;$gpsVal/100" | bc`
        # Minutes part of the lat/long
        gpsMin=`echo "scale=3;$gpsVal-100*$gpsInt" | bc`
        # Convert minutes to a full decimal value
        gpsDec=`echo "scale=5;$gpsInt+$gpsMin/60" | bc`
        # South and West are negative
        if [[ $gpsDir -eq "W" || $gpsDir -eq "S" ]]
        then
            gpsDec="-$gpsDec"
        fi
        echo $gpsDec
    }
    

    Other Considerations

    A GPS module can fail to report location for reasons such as loss of signal, loss of power, or malfunction. Any program that reads GPS values should have a graceful way out if the module isn't responsive or is reporting garbage. My finished program uses a timeout value of 10 seconds to read each sentence, and it reports failure under any of the following scenarios:

    • Failed to read a sentence after ten seconds
    • GPS fix quality is insufficient
    • Location data is invalid
    • Processed 100 sentences but couldn't get a location

    The Finished Program

    First of all, a disclaimer: this program resides on a cell modem I used for testing, and the modem isn't active so I had to retype it rather than copying it. I tried to be careful, but there may be a typo or two.

    And here's the finished program:

    # !bin/sh
    #
    # getloc - read GPS location and echo status, latitude and longitude
    #          separated by a space. Status 0 is success.
    
    function closePort() {
      exec 5<&-
    }
    
    function openPort() {
      exec 5</dev/ttyS3
    }
    
    # Pass the GPS value and direction (N/S/E/W) to get the
    # decimal latitude/longitude.
    function gpsDecimal() {
        gpsVal=$1
        gpsDir ="$2"
        # Integer part of the lat/long
        gpsInt=`echo "scale=0;$gpsVal/100" | bc`
        # Minutes part of the lat/long
        gpsMin=`echo "scale=3;$gpsVal-100*$gpsInt" | bc`
        # Convert minutes to a full decimal value
        gpsDec=`echo "scale=5;$gpsInt+$gpsMin/60" | bc`
        # South and West are negative
        if [[ $gpsDir -eq "W" || $gpsDir -eq "S" ]]
        then
          gpsDec="-$gpsDec"
        fi
        echo $gpsDec
    }
    
    # Return statuses
    STATUS_OK=0
    STATUS_NOFIX_SATDATA=1
    STATUS_TIMEOUT=2
    STATUS_NOTFOUND=3
    STATUS_NOFIX_LOCDATA=4
    
    # Status and counter values
    foundReliability='false'
    foundLocation='false'
    linesRead=0
    
    openPort
    while [[ $linesRead -le 100 && ($foundReliability = 'false' || $foundLocation = 'false') ]]
    do
      # Read the next line from the GPS port, with a timeout of 10 seconds.
      read -t 10 RESPONSE <&5
      if [[ $? -eq 1 ]]
      then
        # Read timed out so bail with error
        closePort
        echo "$STATUS_TIMEOUT 0 0"
        exit 1
      fi
    
      # Fallthrough: line was read. Count it because we have a threshhold
      # for the number of sentences to process before giving up.
      linesRead=`expr $linesRead + 1`
    
      # Get the sentence type.
      sentenceType=`echo $RESPONSE | cut -d',' -f1`
    
      if [[ $sentenceType = '$GPGSA' && $foundReliability = 'false' ]]
      then
        # Found the "fix information" sentence; see if the reliability
        # is at least 2.
        fixValue=`echo $RESPONSE | cut -d',' -f3`
        if [[ $fixValue -ne 2 && $fixValue -ne 3 ]]
        then
          # Insufficient fix quality so bail with error
          closePort
          echo "$STATUS_NOFIX_SATDATA 0 0"
          exit 1
        fi
        # Fallthrough: reliability is sufficient
        foundReliability='true'
      fi # GPGSA sentence
    
      if [[ $sentenceType = '$GPRMC' && $foundLocation = 'false' ]]
      then
        # Found the "recommended minimum data" (GPRMC) sentence;
        # determine if it's "active", which means "valid".
        #
        statusValue=`echo $RESPONSE | cut -d',' -f3`
        if [[ $statusValue = 'V' ]]
        then
          # Void status; can't use the reading so bail
          closePort
          echo "$STATUS_NOFIX_LOCDATA 0 0"
          exit 1
        fi
    
        # Fallthrough: active status, so we can use the reading.
        foundLocation='true'
        latitudeValue=`echo $RESPONSE | cut -d',' -f4`
        latitudeNS=`echo $RESPONSE | cut -d',' -f5`
        latitudeDec=$(gpsDecimal $latitudeValue $latitudeNS)
        longitudeValue=`echo $RESPONSE | cut -d',' -f6`
        longitudeEW = `echo $RESPONSE | cut -d',' -f7`
        longitudeDec=$(gpsDecimal $longitudeValue $longitudeEW)
    
        fi # $GPRMC sentence
    
    done # read-line loop
    
    closePort
    
    # If we get to here and location and reliability were OK, we
    # have a fix.
    if [[ foundReliability = 'true' && foundLocation = 'true' ]]
    then
      echo "$STATUS_OK $latitudeDec $longitudeDec"
      exit 0
    fi
    
    # Fallthrough to here means too many lines were read without
    # finding location information. Return failure.
    echo "$STATUS_NOTFOUND 0 0"
    exit 1
    

Leave a Reply

Your email address will not be published. Required fields are marked *