Library tutorials & articles

Writing Your Own GPS Applications: Part 2

Enforcing Precision

Now that the precision needs of a GPS application can be determined, it's time to find out what source code is necessary to extract and enforce maximum DOP values. All DOP measurements are packaged into the $GPGSA sentence every few seconds. Here is a sample of a $GPGSA sentence:

$GPGSA,A,3,11,29,07,08,5,17,24,,,,,,2.3,1.2,2.0*30

A skillful GPS application developer could know if positional readings are precise enough to use just by looking at one $GPGSA sentence. Again, the best DOP ratings occur when there are several satellites involved in a fix and the satellites are evenly distributed throughout the sky and at separate elevations -– hitting the GPS device from all angles, so to speak.

The last three words of this sentence are 2.3, 1.2, and 2.0, representing mean, horizontal and vertical DOP, respectively. This sentence represents a high-precision environment (suitable enough for both golfing and driving). Using the final listing from part one of this article (Listing 1-8), a method called “ ParseGPGSA ” is added which extracts DOP values and reports them via three events: “ HDOPReceived ”, “ VDOPReceived ” and “ PDOPReceived ”.

C#

//*********************************************************************
//**  A high-precision NMEA interpreter
//**  Written by Jon Person, author of "GPS.NET" (www.gpsdotnet.com)
//*********************************************************************
using System;
using System.Globalization;
public class NmeaInterpreter
{
  // Represents the EN-US culture, used for numers in NMEA sentences
  public static CultureInfo NmeaCultureInfo = new CultureInfo("en-US");
  // Used to convert knots into miles per hour
  public static double MPHPerKnot = double.Parse("1.150779",
    NmeaCultureInfo);
  #region Delegates
    public delegate void PositionReceivedEventHandler(string latitude,
      string longitude);
    public delegate void DateTimeChangedEventHandler(System.DateTime dateTime);
    public delegate void BearingReceivedEventHandler(double bearing);
    public delegate void SpeedReceivedEventHandler(double speed);
    public delegate void SpeedLimitReachedEventHandler();
    public delegate void FixObtainedEventHandler();
    public delegate void FixLostEventHandler();
    public delegate void SatelliteReceivedEventHandler(
      int pseudoRandomCode, int azimuth, int elevation,
      int signalToNoiseRatio);
    public delegate void HDOPReceivedEventHandler(double value);
    public delegate void VDOPReceivedEventHandler(double value);
    public delegate void PDOPReceivedEventHandler(double value);
  #endregion
  #region Events
    public event PositionReceivedEventHandler PositionReceived;
    public event DateTimeChangedEventHandler DateTimeChanged;
    public event BearingReceivedEventHandler BearingReceived;
    public event SpeedReceivedEventHandler SpeedReceived;
    public event SpeedLimitReachedEventHandler SpeedLimitReached;
    public event FixObtainedEventHandler FixObtained;
    public event FixLostEventHandler FixLost;
    public event SatelliteReceivedEventHandler SatelliteReceived;
    public event HDOPReceivedEventHandler HDOPReceived;
    public event VDOPReceivedEventHandler VDOPReceived;
    public event PDOPReceivedEventHandler PDOPReceived;
  #endregion
 
  // Processes information from the GPS receiver
  public bool Parse(string sentence)
  {
    // Discard the sentence if its checksum does not match our
    // calculated checksum
    if (!IsValid(sentence)) return false;
    // Look at the first word to decide where to go next
    switch (GetWords(sentence)[0])
    {
      case "$GPRMC":
        // A "Recommended Minimum" sentence was found!
        return ParseGPRMC(sentence);
      case "$GPGSV":
        // A "Satellites in View" sentence was recieved
        return ParseGPGSV(sentence);
      case "$GPGSA":
        return ParseGPGSA(sentence);
      default:
        // Indicate that the sentence was not recognized
        return false;
    }
  }
  // Divides a sentence into individual words
  public string[] GetWords(string sentence)
  {
    return sentence.Split(',');
  }
  // Interprets a $GPRMC message
  public bool ParseGPRMC(string sentence)
  {
    // Divide the sentence into words
    string[] Words = GetWords(sentence);
    // Do we have enough values to describe our location?
    if (Words[3] != "" & Words[4] != "" &
      Words[5] != "" & Words[6] != "")
    {
      // Yes. Extract latitude and longitude
      // Append hours
      string Latitude = Words[3].Substring(0, 2) + "°";
      // Append minutes
      Latitude = Latitude + Words[3].Substring(2) + "\"";
      // Append hours
      Latitude = Latitude + Words[4]; // Append the hemisphere
      string Longitude = Words[5].Substring(0, 3) + "°";
      // Append minutes
      Longitude = Longitude + Words[5].Substring(3) + "\"";
      // Append the hemisphere
      Longitude = Longitude + Words[6];
      // Notify the calling application of the change
      if(PositionReceived != null)
        PositionReceived(Latitude, Longitude);
    }
    // Do we have enough values to parse satellite-derived time?
    if (Words[1] != "")
    {
      // Yes. Extract hours, minutes, seconds and milliseconds
      int UtcHours = Convert.ToInt32(Words[1].Substring(0, 2));
      int UtcMinutes = Convert.ToInt32(Words[1].Substring(2, 2));
      int UtcSeconds = Convert.ToInt32(Words[1].Substring(4, 2));
      int UtcMilliseconds = 0;
      // Extract milliseconds if it is available
      if (Words[1].Length > 7)
      {
        UtcMilliseconds = Convert.ToInt32(Words[1].Substring(7));
      }
      // Now build a DateTime object with all values
      System.DateTime Today = System.DateTime.Now.ToUniversalTime();
      System.DateTime SatelliteTime = new System.DateTime(Today.Year,
        Today.Month, Today.Day, UtcHours, UtcMinutes, UtcSeconds,
        UtcMilliseconds);
      // Notify of the new time, adjusted to the local time zone
      if(DateTimeChanged != null)
        DateTimeChanged(SatelliteTime.ToLocalTime());
    }
    // Do we have enough information to extract the current speed?
    if (Words[7] != "")
    {
      // Yes.  Parse the speed and convert it to MPH
      double Speed = double.Parse(Words[7], NmeaCultureInfo) *
        MPHPerKnot;
      // Notify of the new speed
      if(SpeedReceived != null)
        SpeedReceived(Speed);
      // Are we over the highway speed limit?
      if (Speed > 55)
        if(SpeedLimitReached != null)
          SpeedLimitReached();
    }
    // Do we have enough information to extract bearing?
    if (Words[8] != "")
    {
      // Indicate that the sentence was recognized
      double Bearing = double.Parse(Words[8], NmeaCultureInfo);
      if(BearingReceived != null)
        BearingReceived(Bearing);
    }
    // Does the device currently have a satellite fix?
    if (Words[2] != "")
    {
      switch (Words[2])
      {
        case "A":
          if(FixObtained != null)
            FixObtained();
          break;
        case "V":
          if(FixLost != null)
            FixLost();
          break;
      }
    }
    // Indicate that the sentence was recognized
    return true;
  }
  // Interprets a "Satellites in View" NMEA sentence
  public bool ParseGPGSV(string sentence)
  {
    int PseudoRandomCode = 0;
    int Azimuth = 0;
    int Elevation = 0;
    int SignalToNoiseRatio = 0;
    // Divide the sentence into words
    string[] Words = GetWords(sentence);
    // Each sentence contains four blocks of satellite information. 
    // Read each block and report each satellite's information
    int Count = 0;
    for (Count = 1; Count <= 4; Count++)
    {
      // Does the sentence have enough words to analyze?
      if ((Words.Length - 1) >= (Count * 4 + 3))
      {
        // Yes.  Proceed with analyzing the block. 
        // Does it contain any information?
        if (Words[Count * 4] != "" & Words[Count * 4 + 1] != ""
          & Words[Count * 4 + 2] != "" & Words[Count * 4 + 3] != "")
        {
          // Yes. Extract satellite information and report it
          PseudoRandomCode = System.Convert.ToInt32(Words[Count * 4]);
          Elevation = Convert.ToInt32(Words[Count * 4 + 1]);
          Azimuth = Convert.ToInt32(Words[Count * 4 + 2]);
          SignalToNoiseRatio = Convert.ToInt32(Words[Count * 4 + 2]);
          // Notify of this satellite's information
          if(SatelliteReceived != null)
            SatelliteReceived(PseudoRandomCode, Azimuth,
            Elevation, SignalToNoiseRatio);
        }
      }
    }
    // Indicate that the sentence was recognized
    return true;
  }
  // Interprets a "Fixed Satellites and DOP" NMEA sentence
  public bool ParseGPGSA(string sentence)
  {
    // Divide the sentence into words
    string[] Words = GetWords(sentence);
    // Update the DOP values
    if (Words[15] != "")
    {
      if(PDOPReceived != null)
        PDOPReceived(double.Parse(Words[15], NmeaCultureInfo));
    }
    if (Words[16] != "")
    {
      if(HDOPReceived != null)
        HDOPReceived(double.Parse(Words[16], NmeaCultureInfo));
    }
    if (Words[17] != "")
    {
      if(VDOPReceived != null)
        VDOPReceived(double.Parse(Words[17], NmeaCultureInfo));
    }
    return true;
  }
  // Returns True if a sentence's checksum matches the
  // calculated checksum
  public bool IsValid(string sentence)
  {
    // Compare the characters after the asterisk to the calculation
    return sentence.Substring(sentence.IndexOf("*") + 1) ==
      GetChecksum(sentence);
  }
  // Calculates the checksum for a sentence
  public string GetChecksum(string sentence)
  {
    // Loop through all chars to get a checksum
    int Checksum = 0;
    foreach (char Character in sentence)
    {
      if (Character == '$')
      {
        // Ignore the dollar sign
      }
      else if (Character == '*')
      {
        // Stop processing before the asterisk
        break;
      }
      else
      {
        // Is this the first value for the checksum?
        if (Checksum == 0)
        {
          // Yes. Set the checksum to the value
          Checksum = Convert.ToByte(Character);
        }
        else
        {
          // No. XOR the checksum with this character's value
          Checksum = Checksum ^ Convert.ToByte(Character);
        }
      }
    }
    // Return the checksum formatted as a two-character hexadecimal
    return Checksum.ToString("X2");
  }
}

VB.NET

' *********************************************************************
' **  A high-precision NMEA interpreter
' **  Written by Jon Person, author of "GPS.NET" (www.gpsdotnet.com)
' *********************************************************************
Imports System
Imports System.Globalization
Public Class NmeaInterpreter
  ' Represents the EN-US culture, used for numers in NMEA sentences
  Public Shared NmeaCultureInfo As New CultureInfo("en-US")
  ' Used to convert knots into miles per hour
  Public Shared MPHPerKnot As Double = Double.Parse("1.150779", _
    NmeaCultureInfo)
  ' Raised when the current location has changed
  Public Event PositionReceived(ByVal latitude As String, _
  ByVal longitude As String)
  Public Event DateTimeChanged(ByVal dateTime As DateTime)
  Public Event BearingReceived(ByVal bearing As Double)
  Public Event SpeedReceived(ByVal speed As Double)
  Public Event SpeedLimitReached()
  Public Event FixObtained()
  Public Event FixLost()
  Public Event SatelliteReceived(ByVal pseudoRandomCode As Integer, _
  ByVal azimuth As Integer, _
  ByVal elevation As Integer, _
  ByVal signalToNoiseRatio As Integer)
  Public Event HDOPReceived(ByVal value As Double)
  Public Event VDOPReceived(ByVal value As Double)
  Public Event PDOPReceived(ByVal value As Double)
  ' Processes information from the GPS receiver
  Public Function Parse(ByVal sentence As String) As Boolean
    ' Discard the sentence if its checksum does not match our
    ' calculated checksum
    If Not IsValid(sentence) Then Return False
    ' Look at the first word to decide where to go next
    Select Case GetWords(sentence)(0)
      Case "$GPRMC"
        ' A "Recommended Minimum" sentence was found!
        Return ParseGPRMC(sentence)
      Case "$GPGSV"
        ' A "Satellites in View" sentence was recieved
        Return ParseGPGSV(sentence)
      Case "$GPGSA"
        Return ParseGPGSA(sentence)
      Case Else
        ' Indicate that the sentence was not recognized
        Return False
    End Select
  End Function
  ' Divides a sentence into individual words
  Public Function GetWords(ByVal sentence As String) As String()
    Return sentence.Split(","c)
  End Function
  ' Interprets a $GPRMC message
  Public Function ParseGPRMC(ByVal sentence As String) As Boolean
    ' Divide the sentence into words
    Dim Words() As String = GetWords(sentence)
    ' Do we have enough values to describe our location?
    If Words(3) <> "" And Words(4) <> "" _
    And Words(5) <> "" And Words(6) <> "" Then
      ' Yes. Extract latitude and longitude
      ' Append hours
      Dim Latitude As String = Words(3).Substring(0, 2) & "°"
      ' Append minutes
      Latitude = Latitude & Words(3).Substring(2) & """"
      ' Append the hemisphere
      Latitude = Latitude & Words(4)       
      ' Append hours
      Dim Longitude As String = Words(5).Substring(0, 3) & "°"
      ' Append minutes
      Longitude = Longitude & Words(5).Substring(3) & """"
      ' Append the hemisphere
      Longitude = Longitude & Words(6)
      ' Notify the calling application of the change
      RaiseEvent PositionReceived(Latitude, Longitude)
    End If
    ' Do we have enough values to parse satellite-derived time?
    If Words(1) <> "" Then
      ' Yes. Extract hours, minutes, seconds and milliseconds
      Dim UtcHours As Integer = CType(Words(1).Substring(0, 2), Integer)
      Dim UtcMinutes As Integer = CType(Words(1).Substring(2, 2), Integer)
      Dim UtcSeconds As Integer = CType(Words(1).Substring(4, 2), Integer)
      Dim UtcMilliseconds As Integer
      ' Extract milliseconds if it is available
      If Words(1).Length > 7 Then UtcMilliseconds = _
        CType(Words(1).Substring(7), Integer)
      ' Now build a DateTime object with all values
      Dim Today As DateTime = System.DateTime.Now.ToUniversalTime
      Dim SatelliteTime As New System.DateTime(Today.Year, Today.Month,
        Today.Day, UtcHours, UtcMinutes, UtcSeconds,
        UtcMilliseconds)
      ' Notify of the new time, adjusted to the local time zone
      RaiseEvent DateTimeChanged(SatelliteTime.ToLocalTime)
    End If
    ' Do we have enough information to extract the current speed?
    If Words(7) <> "" Then
      ' Yes.  Parse the speed and convert it to MPH
      Dim Speed As Double = Double.Parse(Words(7), NmeaCultureInfo)
          * MPHPerKnot
      ' Notify of the new speed
      RaiseEvent SpeedReceived(Speed)
      ' Are we over the highway speed limit?
      If Speed > 55 Then RaiseEvent SpeedLimitReached()
    End If
    ' Do we have enough information to extract bearing?
    If Words(8) <> "" Then
      ' Indicate that the sentence was recognized
      Dim Bearing As Double = Double.Parse(Words(8), NmeaCultureInfo)
      RaiseEvent BearingReceived(Bearing)
    End If
    ' Does the device currently have a satellite fix?
    If Words(2) <> "" Then
      Select Case Words(2)
        Case "A"
          RaiseEvent FixObtained()
        Case "V"
          RaiseEvent FixLost()
      End Select
    End If
    ' Indicate that the sentence was recognized
    Return True
  End Function
  ' Interprets a "Satellites in View" NMEA sentence
  Public Function ParseGPGSV(ByVal sentence As String) As Boolean
    Dim PseudoRandomCode As Integer
    Dim Azimuth As Integer
    Dim Elevation As Integer
    Dim SignalToNoiseRatio As Integer
    ' Divide the sentence into words
    Dim Words() As String = GetWords(sentence)
    ' Each sentence contains four blocks of satellite information. 
    ' Read each block and report each satellite's information
    Dim Count As Integer
    For Count = 1 To 4
      ' Does the sentence have enough words to analyze?
      If (Words.Length - 1) >= (Count * 4 + 3) Then
        ' Yes.  Proceed with analyzing the block. 
        ' Does it contain any information?
        If Words(Count * 4) <> "" And Words(Count * 4 + 1) <> "" _
        And Words(Count * 4 + 2) <> "" And Words(Count * 4 + 3) <> "" Then
          ' Yes. Extract satellite information and report it
          PseudoRandomCode = CType(Words(Count * 4), Integer)
          Elevation = CType(Words(Count * 4 + 1), Integer)
          Azimuth = CType(Words(Count * 4 + 2), Integer)
          SignalToNoiseRatio = CType(Words(Count * 4 + 2), Integer)
          ' Notify of this satellite's information
          RaiseEvent SatelliteReceived(PseudoRandomCode, Azimuth, _
            Elevation, SignalToNoiseRatio)
        End If
      End If
    Next
    ' Indicate that the sentence was recognized
    Return True
  End Function
  ' Interprets a "Fixed Satellites and DOP" NMEA sentence
  Public Function ParseGPGSA(ByVal sentence As String) As Boolean
    ' Divide the sentence into words
    Dim Words() As String = GetWords(sentence)
    ' Update the DOP values
    If Words(15) <> "" Then
      RaiseEvent PDOPReceived(Double.Parse(Words(15), NmeaCultureInfo))
    End If
    If Words(16) <> "" Then
      RaiseEvent HDOPReceived(Double.Parse(Words(16), NmeaCultureInfo))
    End If
    If Words(17) <> "" Then
      RaiseEvent VDOPReceived(Double.Parse(Words(17), NmeaCultureInfo))
    End If
    Return True
  End Function
  ' Returns True if a sentence's checksum matches the calculated checksum
  Public Function IsValid(ByVal sentence As String) As Boolean
    ' Compare the characters after the asterisk to the calculation
    Return sentence.Substring(sentence.IndexOf("*") + 1) = _
      GetChecksum(sentence)
  End Function
  ' Calculates the checksum for a sentence
  Public Function GetChecksum(ByVal sentence As String) As String
    ' Loop through all chars to get a checksum
    Dim Character As Char
    Dim Checksum As Integer
    For Each Character In sentence
      Select Case Character
        Case "$"c
          ' Ignore the dollar sign
        Case "*"c
          ' Stop processing before the asterisk
          Exit For
        Case Else
          ' Is this the first value for the checksum?
          If Checksum = 0 Then
            ' Yes. Set the checksum to the value
            Checksum = Convert.ToByte(Character)
          Else
            ' No. XOR the checksum with this character's value
            Checksum = Checksum Xor Convert.ToByte(Character)
          End If
      End Select
    Next
    ' Return the checksum formatted as a two-character hexadecimal
    Return Checksum.ToString("X2")
  End Function
End Class

Comments

  1. 06 Oct 2008 at 02:06

    quite good and worth a read. confirmed what i needed to know for my research

  2. 03 Aug 2007 at 10:11

    schappi wrote:

    Great paper.



    Thx very much.

  3. 22 Mar 2007 at 01:59


    Hello,
    I do not understand what you mean by writing your own GPS application, the most GPS receivers gives
    navigation data in real time as  UTC, Lat/Lon, SOG, TrueCourse, Magvar,Tracks,constellation sat.
    What we can get more with this application ?
     
     Best regards
     Maritime








  4. 02 May 2006 at 20:11

    Great paper.

     

    Thx very much.

  5. 23 Mar 2006 at 09:27

    If you require Ordnance Survey Grid References, I have written an article on how to do so using NMEA data derived from Jon Person's excellent NMEAinterpreter class.


    GPS- Deriving British Ordnance Survey Grid Referece from NMEA data

    AlexE


  6. 14 Mar 2006 at 12:59

    I spotted this slight error too and made the same correction as cprogrammingguru.
    However this led to a new error as the final word in the string contained the checksum.
    ie in the example sentence

    $GPGSV, 3, 1, 10, 24, 82, 023, 40, 05, 62, 285, 32, 01, 62, 123, 00, 17, 59, 229, 2870

    the final word would be 28
    70
    this caused an error when trying to convert this to an int32 in the line

    SignalToNoiseRatio = Convert.ToInt32(Words[Count * 4 + 3]);

    My solution was to remove the checksum part of the sentence in GetWords, before splitting the sentence
     
        public string[] GetWords(string sentence)
        {
            //remove the final * + checksum
            sentence = sentence.Substring(0, sentence.IndexOf("*"));
            //now split it up
            return sentence.Split(',');
        }


    Assuming that I'm not talking out of my rear, I hope this proves useful
    Alex
     

























  7. 13 May 2005 at 19:49

    This update may help save many lives!


    I love this product. Excellent Job on this product, Jon Person !. I'm really excited infact I am going to go buy and support
    your product 360%!... I just have to correct 1 minor error. I feel you should be aware or maybe your are already....don't know
    might have been a typo,... I'm not much of a C# coder but, I believe during the ParseGPGSV() function, which is suppose to
    parse the "Satellites in View" $GPGSV sentence.... If you look closesly. During the...  public bool ParseGPGSV(string sentence)


    Original Article: http://www.developerfusion.com/show/4652/4/
    Example Sentence: $GPGSV, 3, 1, 10, 24, 82, 023, 40, 05, 62, 285, 32, 01, 62, 123, 00, 17, 59, 229, 28*70


    Each Block consist of 4 words.. "24, 82, 023, 40"  ==  "PseudoRandomCode, Elevation, Azimuth, SignalToNoiseRatio"
    According to your Article... SNR values range from 0-50...where 50 means "Excellent Signal"...though SNR can go as high as  99,
    like you've stated.





    // Interprets a "Satellites in View" NMEA sentence -- section.
    //ERROR suspect


        Azimuth = Convert.ToInt32(Words[Count * 4 + 2]);
        SignalToNoiseRatio = Convert.ToInt32(Words[Count * 4 + 2]);
                                                                             // ^--- Logical Bug, I believe it should be ....


    //CORRECTION
        SignalToNoiseRatio = Convert.ToInt32(Words[Count * 4 + 3]);
                                                                             // ^--- This would be correct.



    Otherwise I believe it would return the same value from the Azimuth extraction. So you wouldn't get any SNR information to
    be able to base precision correctly. For the future of dependability and reliability of code production, I post this correction. As
    far as the Signal Strength of the satellites, within the Notification, of the Event call to,...


             SatelliteReceived ( PseudoRandomCode,  Azimuth,  Elevation, SignalToNoiseRatio );


    I believe it would generate incorrect results...to what ever is going to be done with the SNR variable.
    Please feel free to e-mail me at deciphered_scripturez@yahoo.com for additional details...I really Thank you, Jon Person, for
    your hard work and time put into this and I really would LOVE to help as much as possible...I believe in your product. !


    P.S. This update may save many lives! ... Can you imagine lets say for example, the next block of words over you had,
    "05, 62, 285, 32" in the example above, lets say you passed the third Word of 285 as the value I know 99 would be the highest possible value, but imagine the reliability of the signal being factored in the calculation based on the strength for precision. I assume maybe this would throw off a calculation significantly.


                                                                                                                                                                  Thank You,
                                                                                                                                                                   -- cprogrammingguru

  8. 01 Jan 1999 at 00:00

    This thread is for discussions of Writing Your Own GPS Applications: Part 2.

Leave a comment

Sign in or Join us (it's free).

AddThis

Related podcasts

  • CodeCast Episode 4: State of .NET, IE8, ASP.NET MVC, and O'Reilly Media

    CodeCast Episode 4: State of .NET, IE8, ASP.NET MVC, and O'Reilly MediaHosts Ken Levy and Markus Egger discuss the new State of .NET events, IE8, ASP.NET MVC, followed by an interview from PDC with two editors from O'Reilly Media. More on ASP.NET MVC can be found at http://asp.net/mvc. Interview...

Events coming up

  • Dec 6

    Developing AJAX Web Applications with Castle Monorail

    London, United Kingdom

    Monorail is the model-view-controller engine of the Castle Project, bringing many of the best ideas of Ruby on Rails to the .NET world. In this talk, David De Florinier and Gojko Adzic show how Monorail makes it easy to develop .NET based AJAX applications, and how to use the Castle Project to build Web 2.0 applications effectively. Come to this session if you are a .NET web developer. Everyone is welcome!