Pen Truth

Mike Trethowan

 
 
 

Several years ago, 2000ish I developed a Tower Clock Controller that captured the WWVB signal from Fort Collins, Colorado and converted it to the format used by the Tower Clock Drive Boards manufactured by the company that I work for.  For that project I used a device that was available for Basic Stamp projects.  Later on when those devices became unavailable I redesigned the project for a GPS module.

During the redevelopment I tried several different modules from different manufacturers, so this left me with a lot of unused GPS modules in my prototype collection bin. This project will use the Pharos GPS-360, a USB connected GPS that came with a navigation software popular on PC Laptops before the advent of Smart Phones.  I am sure some of you still have your USB GPS module somewhere.  The GPS outputs NMEA data on a port designated as a Serial Com Port by Windows.  The baud rate is 4800, (as are all NMEA messages.)

The project will use ManagementObjectSearcher to find and set the com port for the GPS.  A Handler will be created to allow us to have a SerialDataReceived Event.  Also, Delegates are used for cross thread access of Labels.  There are several NMEA messages, but we will only focus on two for this Quick Tip, “$GPGSV” and $GPRMC.”  From these two we will get the Date, UTC Time, position data (Longitude/Latitude) reported by the GPS and the Satellites In View data.  These two NMEA sentences provide enough information to understand the other NMEA message types.

Your form will have a SerialPort, six Labels and a Panel.  The SerialPort name will be spGPS, the baud will be 4800.   The Panel size will be 14,14 and the BackColor will be Red.  Name the Labels lblMessage, lblZDA, lblLong, lblLat, lblGSV and lblUTC.  Place the objects where they line up vertically, (you can use the vertical alignment feature of VS2013 to do this easier.)

If you have read my previous tutorials you will know how I setup the code and regions in the main form.  You will need three references, for SystemManagement you may have to add it to the list of References in the Project Properties tab, (hint: you will have to right click the project folder and select Properties, not the Solution in the Solution Explorer.)

Imports System.IO.Ports
Imports System.Management
Imports System.ComponentModel

For the Dims, rxMessage is the string that holds the data collected from the GPS.  DataType() is the string array that the message data types will get parsed to. VidPid is the Vendor ID and the Product ID of this particular GPS.  If you use a different model you can get this information from the Device Manager in the Windows Control Panel.  If you are using Windows 8 and above, you can search for it using the keywords “Device Manager”.

#Region " Dims "
    Dim rxMessage As String
    Dim DataType() As String
    Const VidPid As String = "VID_067B&PID_AAA0"
#End Region

The program needs to find the GPS.  This can be done at startup without any input needed from the user in the Form_Load routine (which is why there are no Buttons in this project.)  When the GPS is located the routine automatically assigns the com port and opens it.  We exit the For statement once the GPS is located as we no longer need the Searcher slowing things up.  The value for foundGPS is tested against -1 as Indexes can be assigned 0 to x.

#Region " Main Routines "

    Private Sub FindGPS()
        Dim searcher As New ManagementObjectSearcher("root\WMI", "SELECT * FROM MSSerial_PortName")
        For Each queryObj As ManagementObject In searcher.Get()
            Dim foundGPS As Integer = queryObj("InstanceName").IndexOf(VidPid)
            If foundGPS > -1 Then
                lblMessage.Text = "GPS Found At " & queryObj("PortName")
                spGPS.PortName = queryObj("PortName")
                System.Threading.Thread.Sleep(100)
                spGPS.Open()
                Exit For
            End If
        Next
    End Sub

#End Region

Also add the routines that the program will be accessing on loading and closing.

#Region " Main Routines "

    Private Sub StartUp()
        lblMessage.Text = ""
        findGPS()
        AddHandler spGPS.DataReceived, AddressOf GetMessage
    End Sub

    Public Function FindGPS() As Boolean
        Dim answer As Boolean = False
        Dim searcher As New ManagementObjectSearcher("root\WMI", "SELECT * FROM MSSerial_PortName")
        For Each queryObj As ManagementObject In searcher.Get()
            Dim foundGPS As Integer = queryObj("InstanceName").IndexOf(VidPid)
            If foundGPS > -1 Then
                lblMessage.Text = "GPS Found At " & queryObj("PortName")
                answer = True
                spGPS.PortName = queryObj("PortName")
                System.Threading.Thread.Sleep(100)
                spGPS.Open()
                Exit For
            End If
        Next
        Return answer
    End Function

    Private Sub WrapUp()
        spGPS.Close()
        spGPS.Dispose()
    End Sub

#End Region

To get all of this going at run time there has to be an event for the loading of the form and to wrap up when the program is closed there has to be a Closing event with calls to the routines we want to run.  While VS2013 will automatically create the Load event when you double click the form, it doesn’t create the Closing event.

#Region " Form Loading & Closing "

    Private Sub Form1_Load(sender As Object, e As EventArgs) Handles MyBase.Load
        StartUp()
    End Sub

    Private Sub Form1_Closing(sender As Object, e As EventArgs) Handles Me.Closing
        WrapUp()
    End Sub

#End Region

The Delegate allows cross thread activity so that the GetMessage() routine can access Labels without creating exceptions at run time.  There are two parts to the Delegate, the Delegate itself and the subroutine that handles the Label text.  Also, if you don’t need a MessageBox, send the Exceptions to a Label.  Include the Routine’s name so that you know where the Exception took place.  This is a much cleaner way of presenting messages.  This also has another benefit; If you use a message box, those message boxes can quickly multiply if they are within an event that continues to run after the exception is thrown.

#Region " Delegates "

    Private Delegate Sub MessageDelagate(ByVal Lbl As Label, ByVal txt As String)

    Private Sub DisplayMessage(ByVal Lbl As Label, ByVal txt As String)
        Try
            If Lbl.InvokeRequired Then
                Lbl.Invoke(New MessageDelagate(AddressOf DisplayMessage), New Object() {Lbl, txt})
            Else
                Lbl.Text = txt
            End If
        Catch ex As Exception
            lblMessage.Text = "Error In DisplayMessage() " & ex.Message
        End Try
    End Sub

#End Region

Lets create the serial event routine.  The message string gets set to nothing so we can test it for valid data later on.  We use ReadLine as the NMEA data is sent in sentences which we will have to parse later on in the routine TranslateNMEA().  As GetMessage() is an Event there is no need for Timers.

    Private Sub GetMessage(sender As Object, e As SerialDataReceivedEventArgs)
        Try
            rxMessage = ""
            If spGPS.IsOpen And spGPS.BytesToRead Then
                rxMessage = spGPS.ReadLine
                TranslateNMEA(rxMessage)
                pnlRx.BackColor = Color.Green
            Else
                pnlRx.BackColor = Color.Red
                spGPS.Open()
            End If
        Catch ex As Exception
            DisplayMessage(lblMessage, "Error In <code class="vbnet plain">GetMessage() " &amp;</code> ex.Message)
        End Try
    End Sub

TranslateNMEA() is where we evaluate the message that was just received.  There are several NMEA messages, but we will only focus on three for this Quick Tip.  A Select Case statement is used to evaluate the message header and the appropriate sub routine is called.

#Region " NMEA Routines "

    Public Function TranslateNMEA(ByVal msg As String) As Boolean
        DataType = msg.Split(","c)
        Select Case DataType(0)
            Case "$GPGSV" : GSV()
            Case "$GPRMC" : RMC()
        End Select
    End Function

#End Region

The last two pieces of the puzzle.  Place these in the NMEA Routines region.  Here you can see how the Delegates are used.  If these didn’t exist the program would throw exceptions every time the Labels were accessed by GetMessage().  Even though GetMessage() does not directly access the Labels, the routines called through GetMessage() are part of the same thread.

    Private Sub GSV()
        DisplayMessage(lblGSV, "Satellites In View: " & DataType(3))
    End Sub

    Private Sub RMC()
        Dim dateIs As String = DataType(9)
        Dim utcIs As String = DataType(1)
        utcIs = utcIs.Insert(2, ":")
        utcIs = utcIs.Insert(5, ":")
        dateIs = dateIs.Insert(2, "/")
        dateIs = dateIs.Insert(5, "/")
        DisplayMessage(lblLat, "Latitude: " & DataType(3) & " " & DataType(4))
        DisplayMessage(lblLong, "Longitude: " & DataType(5) & " " & DataType(6))
        DisplayMessage(lblZDA, "Date: " & dateIs)
        DisplayMessage(lblUTC, "Time UTC: " & utcIs)
    End Sub

While this is not a fancy program, it does demonstrate the logic.

To learn more about the NMEA standard, go to http://www.gpsinformation.org/dale/nmea.htm

Disclaimer:  The code in this tutorial is not intended as a final product or by no means the only way this project can be coded. The presented code is for educational purposes only and no warranty is granted or implied. I/we are not responsible for any damages as a result of using this code. Use at your own risk.

Comments are closed.