Entries:
Comments:
Posts:

Loading User Information from Channel 9

Something went wrong getting user information from Channel 9

Latest Achievement:

Loading User Information from MSDN

Something went wrong getting user information from MSDN

Visual Studio Achievements

Latest Achievement:

Loading Visual Studio Achievements

Something went wrong getting the Visual Studio Achievements

Where the Heck am I? Connecting .NET 2.0 to a GPS

 

This article explains how to connect to GPS device using .Net 2.0 and determine someone's location.

Upgrading an Application

I picked up the new Microsoft Streets and Trips 2005 with GPS and frankly I had pretty low expectations. I mean, how good could a $100 GPS possibly be? Well, pretty darn good. Not only is it TINY, but it uses the simple NMEA GPS Data standard. When you plug in the GPS via USB a virtual serial port is installed that lets you talk to the GPS as if it were just any old COM Port.  This GPS speaks at 4800bps 8N1. If you open up HyperTerminal the GPS will just start spewing.

Then I noticed that EdJez had started working on a .NET 1.1 application to speak NMEA to GPSs so I remembered the first rule of programming - never write what you can beg, borrow or steal. I emailed Ed and his crew said I could inherit their .NET 1.1 work in progress. This meant that not only would I be able to talk to a GPS with .NET 2.0, but I'd also get to experience a pretty good upgrade scenario.

Gps2

I read through the newly-converted-to-2.0 code and came up with a few things I'd like to work on. The original .NET 1.1 project consisted of a GPS Library that handled serial port IO and the NMEA Protocol, and a Windows Forms Executable called GPS Client that used the GPS Library. Remember that .NET 1.x didn't include managed support for talking to serial ports, so it used a technique called "Platform Invoke" or P/Invoke to talk directly to the non-.NET Win32 (Application Programming Interface) API. The Win32 methods aren't nearly as friendly as .NET and this particular library spent 400 lines of code just managing the serial ports.

Fortunately .NET 2.0 includes the new System.IO.Ports, so I figured I'd be able to use these. I started by opening the 1.1 Solution (.SLN) File into Visual Studio 2005 which launched an Upgrade Wizard. The Upgrade Wizard created a backup folder and reported no errors during the conversion of both projects! Fabulous. Now, even though the upgrade worked perfectly and the application ran unchanged, I did receive a number of warnings during compilation that I wanted to fix.

Also, because there were so many ways to hack serial port communication in .NET 1.1 there's no clean way for the Upgrade Wizard to convert the code to use System.IO.Ports. That means that the Upgrade Wizard does what it can to get your application running - very often without any code changes at all - but doesn't make design suggestions.

First, I took a chance and deleted the SerialHelper.cs and SerialDCB.cs files that contained the P/Invoke code. That means I literally deleted an entire subsystem. I knew that the application wouldn't compile after I'd done this, but it was a quick way to find out exactly how tangled the serial port code was. Don't be afraid to do brash experiments like this when you're working with your own code. The worst case scenario means you waste some time and you have to undo your change, but the best case scenario can be pretty great. I call this "programming by deletion." I figured there was a pretty good chance that reading from a serial port is a problem that's been solved pretty well and I assumed .NET 2.0 would get it right.

In this case, it turned out that the GPS Client only opened, read, then closed the serial ports using the old library. (That means there were 400 lines of code in the 1.1 version dedicated to supporting just these three methods!) It was pretty easy then to hook up the System.IO.Ports.SerialPort methods and move on to the next few small cosmetic issues.

Previous 1.x code

C#

//serialHelper contains 200 lines of P/Invoke code, 
// is specific to this application and has the 
// speed and details hardcoded inside a method called InitializeserialHelper.

COMPort = (string)COMlistBox.SelectedItem;
serialHelper.Initialize();

New 2.0 code

C#

//serialHelper.COMPort = (string)COMlistBox.SelectedItem;
//serialHelper.Initialize();
port.PortName = COMlistBox.SelectedItem as string;
port.Parity = Parity.None;
port.BaudRate = 4800;
port.StopBits = StopBits.One;
port.DataBits = 8;
port.Open();


Visual Basic
port.PortName = CType(COMlistBox.SelectedItem, String)
port.Parity = Parity.None
port.BaudRate = 4800
port.StopBits = StopBits.One
port.DataBits = 8
port.Open()

The new code is more explicit and very simple. The read() code changed also:

Previous 1.x code

C#

byte[] bData = null;
bData = serialHelper.Read();
protocol.ParseBuffer(bData)

 

The old code was totally custom and embedded in the SerialHelper library. Note that bData is initialized to null but is returned from the .Read() method. The buffer was being allocated inside the old library. Now that we are using the generic .NET 2.0 SerialPort library we had to be more explicit and own our buffer.

New 2.0 code

C#

byte[] bData =  new byte[256];
//bData = serialHelper.Read();
port.Read(bData, 0, 256);
protocol.ParseBuffer(bData);


Visual Basic
Dim bData(255) As Byte
'bData = serialHelper.Read();
port.Read(bData, 0, 256)
protocol.ParseBuffer(bData)

Other Opportunities for Improvement

There was a HashTable in the previous version that associated an integer satelliteid with a Satellite object. The collection in the previous version was totally custom, required about 20 lines of miscellaneous code and was used like this:

C#

foreach(Satellite sat in protocol.GPGSV.Satellites.satCollection.Values)


Visual Basic
Dim sat As Satellite
For Each sat In  protocol.GPGSV.Satellites.satCollection.Values

Since .NET 2.0 includes support for generics which allow you to use a syntax like Dictionary<int, Satellite> to create a strongly typed collection entirely at runtime. The usage doesn't change much; it becomes slightly simpler. However, the whole custom collection class from the 1.x versions goes away and we yank another 20 lines without losing any functionality.

foreach(Satellite sat in protocol.GPGSV.Satellites.Values)

These kind of small, but significant changes are the kinds you'll find yourself coding if you ever have the opportunity to upgrade an application to from .NET 1.x to .NET 2.0. Things get simpler, can be done in fewer lines of code and just work.

The original application didn't do any resizing of the component controls. Writing a WinForms application that looks polished is difficult for most programmers. Getting the spacing of controls correct is difficult and folks, myself included, just don't have the patience to get the spacing and resizing correct. WinForms and the Visual Studio WinForms Designer has always supported a feature called "anchoring" that lets you keep one edge anchored to a point on the form while another edge or edges moves as the form resizes. I added resizing for the whole application, but particularly the Satellite View since it's always cool when graphics resize smoothly, eh? 

Gps3

NMEA Command and Data processing

The GPS Library exposes an object model that represents the NMEA standard. The National Marine Electronics Association (NMEA) created a standard data format consisting of a series of data sentences. Each sentence begins with a '$' and ends with a carriage return/line feed sequence. The data is contained within this single line with data items separated by commas. Here's an example from reference web page of a "Satellites in View" sentence. The Raw NMEA tab fills a text box with the actual data string. That raw data tabs was all I needed to add support for new NMEA data sentences.

$GPGSV,2,1,08,01,40,083,46,02,17,308,41,12,07,344,39,14,22,228,45*75

Where:
      GSV          Satellites in view
      2            Number of sentences for full data
      1            sentence 1 of 2
      08           Number of satellites in view

      01           Satellite PRN number
      40           Elevation, degrees
      083          Azimuth, degrees
      46           SNR - higher is better
                   for up to 4 satellites per sentence
      *75          the checksum data, always begins with *

The first word after $ is the command. There are over a dozen commands depending on which version of the specification your GPS supports. The slick thing about this simple format is that if your program doesn't understand a command it can ignore it. The original 1.1 application only supported a few commands, and I added a few more for this article. Each sentence is validated against checksum value at the end to ensure data integrity.

After we've found the command after the $, processing it is straightforward and extensibility is just adding a case to a switch statement:

C#

public void ProcessCommand(string sCmd, byte[] bData)
{
    string data = EncodeToString(bData);
    switch(sCmd)
    {
        case "GPGGA":
            ProcessGPGGA(data);
            break;
        case "GPGSA":
            ProcessGPGSA(data);
            break;
        case "GPGSV":
            ProcessGPGSV(data);
            break;
        case "GPRMC":
            ProcessGPRMC(data);
            break;
        case "GPRMB":
            //ProcessGPRMB(pData);
            break;
        case "GPZDA":
            //ProcessGPZDA(pData);
            break;
        default:
            break;
    }
    CommandCount = CommandCount + 1;
}


Visual Basic
Public Sub ProcessCommand(sCmd As String, bData() As Byte)

    Dim data As String = EncodeToString(bData)
    Select Case sCmd

        Case "GPGGA"
            ProcessGPGGA(data)
            
        Case "GPGSA"
            ProcessGPGSA(data)
            
        Case "GPGSV"
            ProcessGPGSV(data)
            
        Case "GPRMC"
            ProcessGPRMC(data)
            
        Case "GPRMB"
            'ProcessGPRMB(pData);
            
        Case "GPZDA"
            'ProcessGPZDA(pData);
            
        Case Else
    End Select
    CommandCount = CommandCount + 1
End Sub 'ProcessCommand

Each ProcessXXXXX method takes the comma separated data after the command and loads it into an object model. For example, the GPGSV (Satelllites in View) command can be created by reading the spec above and applying it. Before any commands are passed to ProcessCommand() their integrity is validated against the trailing checksum value.

C#

public void ProcessGPGSV(string data)
{
    //parses the GPGSV stream to extract satellite information
    string[] fields = Regex.Split(data,",");
    uint totalNumberOfMessages = Convert.ToUInt32(fields[0]);

    //make sure the data is OK. valid range is 1..8 channels
    if ((totalNumberOfMessages > 8) || (totalNumberOfMessages <= 0))
        return;

    GPGSV.TotalNumberOfMessages = totalNumberOfMessages;

    //message number
    int nMessageNumber = Convert.ToInt32(fields[1]);
    
    //make sure it is 0..9...
    if ((nMessageNumber > 9) || (nMessageNumber < 0))
        return;

    //sats in view
    GPGSV.SatellitesInView  = Convert.ToInt32(fields[2]);

    for (int iSat = 0; iSat < 4; iSat++)
    {
        Satellite sat = new Satellite();
        sat.Id = Convert.ToInt32(fields[3+iSat*4]);
        sat.Elevation = Convert.ToInt32(fields[4+iSat*4]);
        sat.Azimuth = Convert.ToInt32(fields[5+iSat*4]);
        sat.SignalQuality = Convert.ToInt32(fields[6+iSat*4]);
        sat.Used = IsSatelliteUsed(sat.Id);
        
        GPGSV.Satellites.Add(sat);
    }
    GPGSV.Count ++;
}


Visual Basic
Public Sub ProcessGPGSV(data As String)
    'parses the GPGSV stream to extract satellite information
    Dim fields As String() = Regex.Split(data, ",")
    
    Dim totalNumberOfMessages As System.UInt32 = 
        Convert.ToUInt32(fields(0)) 
    'ToDo: Unsigned Integers not supported
        
    'make sure the data is OK. valid range is 1..8 channels
    If totalNumberOfMessages > 8 OrElse totalNumberOfMessages <= 0 Then
        
    Return
    
    End If 
      
    GPGSV.TotalNumberOfMessages = totalNumberOfMessages
    
    'message number
    Dim nMessageNumber As Integer = Convert.ToInt32(fields(1))
    
    'make sure it is 0..9...is there an inconsistency? 8/9?
    If nMessageNumber > 9 OrElse nMessageNumber < 0 Then
    
        Return
      
    End If 
    
    'sats in view
    GPGSV.SatellitesInView = Convert.ToInt32(fields(2))
    
    'for(int iSat = 0; iSat < GPGSV.SatellitesInView; iSat++)
    Dim iSat As Integer
    
    For iSat = 0 To 3
        Dim sat As New Satellite()
            
        sat.Id = Convert.ToInt32(fields((3 + iSat * 4)))
      
        sat.Elevation = Convert.ToInt32(fields((4 + iSat * 4)))
      
        sat.Azimuth = Convert.ToInt32(fields((5 + iSat * 4)))
      
        sat.SignalQuality = Convert.ToInt32(fields((6 + iSat * 4)))
      
        sat.Used = IsSatelliteUsed(sat.Id)
            
        GPGSV.Satellites.Add(sat)
    Next iSat
    
    GPGSV.Count += 1
    
End Sub 'ProcessGPGSV

The now-loaded object model loaded with data is then used to paint satellites on the WinForm and load various labels with data.

Painting the Satellites

The most fun part of any WinForms application for me is custom painting. The Satellite Tracking tab in this application takes the newly loaded GPGSV object and presents it visually. It's important to note that we're not doing the painting directly from the raw data string. If you're a new programmer it may seem like extra work, but it's much easier to "refine" the physical data (the data on the wire) into a logical form (our GPS object model) then do processing, painting or calculations on the logical model. You'll be partially insulated if the raw data should change in future. You'll find that encapsulation, or data hiding (even from yourself), is the most powerful aspect of Object Oriented Programming. Why should I have to sweat the details of the serial point and data format when I just want paint some satellites? By moving the data through multiple finite and simple bite-sized stages rather than one super-transformation you'll be able to create applications that are larger and more functional than any you've created before. OOP allows you to focus on the problem at hand while trusting in the other subsystems of your larger program.

Gps1

Custom painting code in .NET is a joy. We create a Graphics context from a picture box that we'll be painting to. We paint a small Rectangle for each Satellite in the Satellite dictionary<int, Satellite> collection while doing some math on the azimuth and elevation values and map them to the circles and x,y coordinate system of the picture box. The picture box may resize and the satellites will move accordingly.

C#

private void DisplaySatellites()
{
    Pen circlePen = new Pen(System.Drawing.Color.DarkBlue,1);
    Graphics g = picSats.CreateGraphics();
    int centerX = picSats.Width/2;
    int centerY = picSats.Height/2;
    double maxRadius = (Math.Min(picSats.Height,picSats.Width)-20) / 2;

    //draw circles
    double[] elevations = new double[] {0,Math.PI/2, Math.PI/3 ,
        Math.PI / 6};
    foreach(double elevation in elevations)
    {
        double radius = (double)System.Math.Cos(elevation) * maxRadius;
        g.DrawEllipse(circlePen,(int)(centerX - radius) ,
                     (int)(centerY - radius),
                     (int)(2 * radius),(int)( 2*  radius));
    }
    //90 degrees elevation reticule
    g.DrawLine(circlePen,new Point(centerX-3,centerY),
               new Point(centerX + 3,centerY));
    g.DrawLine(circlePen,new Point(centerX,centerY-3),
               new Point(centerX,centerY+3));

    Pen satellitePen = new Pen(System.Drawing.Color.LightGoldenrodYellow,4);
    foreach(Satellite sat in protocol.GPGSV.Satellites.Values)
    {            
        //SNIP...

        //draw satellites
        double h = (double)System.Math.Cos(
                   (sat.Elevation*Math.PI)/180) * maxRadius;
        
        int satX = (int)(centerX + h * Math.Sin(
                   (sat.Azimuth * Math.PI)/180));
        int satY = (int)(centerY - h * Math.Cos(
                   (sat.Azimuth * Math.PI)/180));

        g.DrawRectangle(satellitePen,satX,satY, 4,4);
        g.DrawString(sat.Id.ToString(), 
                     new Font("Verdana", 8, FontStyle.Regular), 
                     new System.Drawing.SolidBrush(Color.Black), 
                     new Point(satX + 5, satY + 5));
    }
}


Visual Basic
Private Sub DisplaySatellites() 
    labelSatellitesInView.Text = 
        protocol.GPGSV.SatellitesInView.ToString()
    Dim circlePen As New Pen(System.Drawing.Color.DarkBlue, 1)
   
    Dim g As Graphics = picSats.CreateGraphics()
    Dim centerX As Integer = picSats.Width / 2
    Dim centerY As Integer = picSats.Height / 2
    Dim maxRadius As Double = (Math.Min(picSats.Height, picSats.Width)
         - 20) / 2
        
    'draw circles
    Dim elevations() As Double = {0, Math.PI / 2, Math.PI / 3, 
        Math.PI / 6}
    Dim elevation As Double
 
    For Each elevation In  elevations
        Dim radius As Double =
            System.Convert.ToDouble(System.Math.Cos(elevation)) *
                maxRadius
   
      g.DrawEllipse(circlePen, _CType(Fix(centerX - radius), Single),
                               _CType(Fix(centerY - radius), Single),
                               _CType(Fix(2 * radius), Single),
                               _CType(Fix(2 * radius), Single))
    Next elevation
    '90 degrees elevation reticule

    g.DrawLine(circlePen, New Point(centerX - 3, centerY), 
        New Point(centerX + 3, centerY))

   g.DrawLine(circlePen, New Point(centerX, centerY - 3), 
        New Point(centerX, centerY + 3))
        
    Dim satellitePen As New Pen(System.Drawing.Color.LightGoldenrodYellow,
    4)
    Dim sat As Satellite
 
    For Each sat In  protocol.GPGSV.Satellites.Values
    'if has a listitem

        Dim lvItem As ListViewItem = CType(sat.Thing, ListViewItem)
        
        If lvItem Is Nothing Then
            lvItem = New ListViewItem(New String() 
                {sat.Id.ToString(), sat.Elevation.ToString(),
                sat.Azimuth.ToString(), sat.Used.ToString()})

            listSatellites.Items.Add(lvItem)
             sat.Thing = lvItem 'lvItem;
        Else
            lvItem.Text = sat.Id.ToString()
            lvItem.SubItems(1).Text = sat.Elevation.ToString()
            lvItem.SubItems(2).Text = sat.Azimuth.ToString()
            lvItem.SubItems(3).Text = sat.Used.ToString
        End If
        
        'draw satellites
        Dim h As Double = 
            System.Convert.ToDouble(System.Math.Cos(sat.Elevation * 
                Math.PI / 180)) * maxRadius
        Dim satX As Integer = Fix(centerX + h * Math.Sin(sat.Azimuth *
             Math.PI / 180))
        Dim satY As Integer = Fix(centerY - h * Math.Cos(sat.Azimuth * 
            Math.PI / 180))
        g.DrawRectangle(satellitePen, satX, satY, 4, 4)
        g.DrawString(sat.Id.ToString(), New Font("Verdana", 8,
            FontStyle.Regular), New System.Drawing.SolidBrush(
            Color.Black), New Point(satX + 5, satY + 5))
    Next sat
End Sub 'DisplaySatellites

Conclusion

At this point, we've got a .NET 2.0 WinForms Application that will listen to any NMEA compliant GPS on a serial port. It receives data from a serial port, loads it into objects in memory, then updates the WinForm using the data in those objects. Here's some ideas of other things you can do yourself to extend this project:

  • Implement the complete NWEA specification, or just add one or two new data sentences.
  • Use the MapPoint Web Service to retrieve an image and paint it on the WinForm.
  • Create a tab that creates "geo-art" by drawing a location trail as you move.

Enjoy expanding on the existing project and remember not to fear the words "Some Assembly Required!"


Big thanks to Edward Jezierski for the original 1.1 app, and to Daniel Cazzullino and Eugenio Pace for their help!

Follow the Discussion

  • shahshah

    It's great if you could convert this into .Net CF (VB)

  • Troy BrownTroy Brown

    Great read. I found this while looking for ways to port my current NMEA embedded C code into a windows app. Thanks for doing a lot of the work for me!

  • This is also freezing for me when I am using a This is also freezing for me when I am using a

    This application is freezing with my Garmin eTrex at line 692, any ideas? I can be emailed at michael.dance(@)gmail.com

  • imominimomin

    Try my gps string parser http://www.gpsxml.com/gpsxml/service.asmx?op=GPS2XML

    I need somebody to test my mobile APP. You can download the cab file from http://gps.gpsxml.com/viewtopic.php?t=4">http://gps.gpsxml.com/viewtopic.php?t=4

    http://gps.gpsxml.com/tracker.cfm?userId=1">http://gps.gpsxml.com/tracker.cfm?userId=1

    Please feel free to give any comments, suggestions or ideas

    Thanks

    Imtiyaz Momin

    http://gps.gpsxml.com/

    imtu80@hotmail.com

  • Clint RutkasClint I'm a "developer"

    Do have a GPS receiver laying around? Do you travel? Do you blog? If you answered yes to these questions,

  • ronironi

    i developped a similar class to these but i am not getting exact positions....same procedure as in here...am i missing something?

  • jamesjames

    Nice sample.  I found two bugs in NMEAProtocol.cs as an FYI -

    ParseBuffer - You need to check the byte for '\0' and break inside the foreach.  If not, then you will insert nulls from end of serial buffer into the data buffer.

    This results in corrupt data.

    ProcessGPGSV - not critical, but there aren't always 4 sats.  I used (int)((fields.Length - 3) / 4) instead.  Otherwise you loose the last couple of sats if there is less than 4 in that packet (throws out entire packet).

  • Gurdev SinghGurdev Singh

    Can we run SmartPhone version of this application on Windows Mobile and access the GPS information without any additional/external GPS device?

  • SmartymobileSmartymobile

    Uno de los parámetros mas usados en lectura de GPS s bajo protocola NMEA puede ser el GPGGA Hoy estoy

  • JochenJochen

    What SignalQuality is neccessary, so that the satelite can be used?

    How can I calculate a the signal quality of all data? I want to show my users a value between 0 - 5 (0 == bad quality; 5 == awesome quality), but how can I calculate such a value?

  • FrankFrank

    I think you can't feed the azimuth directly into the Math.Cos function! Azimuth goes from 0° top-mid to 90° right-mid in a circle.. So to convert the azimuth to be used with cosine function you need to use (-90 - azimuth) instead. However I see you nicely hacked that up by using the sine function for the X coordinate and cosine for Y coordinate Smiley

  • jimezamjimezam

    Very interesting.  This is exactly what I need.  But I have a problem.  I have an eTrex Summit HC and it is working fine but its driver doesn't install the virtual COM port that the tutorial shows, just an USB.

    Can you help me with this ?

    Thank you for your help.

  • Rachid KacelRachid Kacel

    It seems that the convert.ToDouble(String s) needs a formatprovider for my computer (Vista english in France) , otherwise the decimal point (a comma is expected) generates a formatException

  • Clint RutkasClint I'm a "developer"

    something like this will fix that issue: x.ToString(CultureInfo.InvariantCulture);

  • trenttrent

    When I try to start the app I get a "System.IO.IOException" when it trys to port.Open(); (line 658 in MainForm.cs). I haven't modifyed any code. MS Visual C# 2008 Express Edition running on Windows 7 RC

  • Clint RutkasClint I'm a "developer"

    this uses the GPS that came with Streets and Trips a few years back and mounts as a serial port device.  Are you sure you have the GPS hooked in and it is set to the proper port?  Is there another program that is currently using the GPS when you're attempting to run this program?

  • Clint RutkasClint I'm a "developer"

    @trent:  email us via the contact button, we'll take this offline.

  • trenttrent

    I'm absolutely positiv that it mounts as a serial port device and that there are no other programs trying to read data from it. I will post a more detailed description of the error when I get home from work Smiley  

  • LuisLuis

    Hi,

    thanks for your example Smiley

    You have a little bug in C# code when ProcessGPGGA:

    //Longitude

    GPGGA.Longitude = Convert.ToDouble(fields[3])/100;

    if(fields[4]=="E")

    GPGGA.LongitudeHemisphere = Cardinal.East;

    else

                       GPGGA.LongitudeHemisphere = Cardinal.West;

    Originally you set East/West to Latitude Hemisphere (copy/paste bug ;P)

    Regards.

  • Clint RutkasClint I'm a "developer"

    @milindu, I think there are mapping apps out there that can do that.  

  • MilinduMilindu

    how does this work ?? i really dunt understand im a new bie ... can i conneect this to my GPS through GPRS and then pass the values on to google maps .. ??

  • Clint RutkasClint I'm a "developer"

    @Djogi Does your GPS give you the same lat / long in a different application?

  • DjogiDjogi

    Belgrade's position is 44.79, 20:47, but the program finds 44.47, 20:28

    What is the problem

  • ShahzadShahzad

    This is really awesome link, I thank you for this big help for those who really want to learn.

Remove this comment

Remove this thread

close

Comments Closed

Comments have been closed since this content was published more than 30 days ago, but if you'd like to continue the conversation, please create a new thread in our Forums,
or Contact Us and let us know.