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

Published 31 October 06 09:25 AM | Coding4Fun 
  This article explains how to connect to GPS device using .Net 2.0 and determine someone’s location.
Scott Hanselman's Computer Zen

Difficulty: Intermediate
Time Required: 3-6 hours
Cost: $100-$200
Software: Visual Studio Express Editions
Hardware:
Download: Download

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

//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

Visual 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

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

Visual 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:

Visual 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:

Visual 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.

Visual 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.

Visual 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!

Comment Notification

If you would like to receive an email when updates are made to this post, please register here

Subscribe to this post's comments using RSS

Comments

# bjwjun said on November 16, 2006 8:58 PM:

the explains how to connect to GPS device using .Net 2.0 and determine someone’s location

for ppc

# jeepx.net » Blog Archive » DIY: C# Code for GPS said on January 16, 2007 2:24 AM:

PingBack from http://www.jeepx.net/?p=271

# Kevin said on February 19, 2007 4:19 PM:

Does this support Garmin devices

# shah said on April 22, 2007 7:30 PM:

Could you convert this into .Net Compact Framework. That should be great for me.

# shah said on April 22, 2007 7:35 PM:

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

# Troy Brown said on May 9, 2007 11:50 AM:

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 said on July 18, 2007 6:38 PM:

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

# imomin said on August 19, 2007 3:24 AM:

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

# Coding4Fun said on August 31, 2007 6:17 PM:

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

# roni said on October 3, 2007 6:38 AM:

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

# james said on December 7, 2007 8:46 PM:

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 Singh said on January 24, 2008 7:22 AM:

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

# Smartymobile said on February 19, 2008 1:56 PM:

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

# » Where the Heck am I? Connecting .NET 2.0 to a GPS MindFeed said on February 20, 2008 8:10 PM:

PingBack from http://jabnet.dyndns.org:8008/wordpress/?p=15

# Jochen said on March 22, 2008 7:01 PM:

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?

# Frank said on April 25, 2008 8:09 AM:

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 :)

# jimezam said on September 10, 2008 3:17 PM:

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 Kacel said on December 11, 2008 5:56 AM:

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

# Coding4Fun said on December 29, 2008 5:32 PM:

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

# trent said on May 10, 2009 11:38 AM:

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

# Coding4Fun said on May 11, 2009 11:56 AM:

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?

# trent said on May 12, 2009 8:26 AM:

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 :-)  

# Coding4Fun said on May 12, 2009 1:29 PM:

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

Leave a Comment

(required) 
(optional)
(required) 
Page view tracker