Pocket GPS

8/1/2013

I've just been browsing through my \Dev folder, and rediscovered the Pocket GPS app I wrote as a side project in 2002. I was even more surprised when I loaded it into Visual Studio 2008, compiled it, and it actually still works!

Compass rose   Satellites   Routes
Compass rose Satellites Routes
 
Edit route   Waypoints   Edit waypoint
Edit route Waypoints Edit waypoint

This project was interesting. First, I used to have a thing for GPS back then, and I had a lot of fun creating an RS232 library for the .NET Compact Framework. It ended up being published in the Microsoft .NET Compact Framework (Core Reference). The app itself is pretty simple -

Pocket GPS sequence diagram

The second reason that this remains a memorable project is the kick I got out of parsing and interpreting NMEA 0183 sentences (superseded in the meantime by NMEA 2000), and being able to produce the satellite location and signal strength indicators you see in the second image above. Looking at the code today it seems trivial, and I wonder what the big deal was in 2002...

I've pasted the code to parse NMEA 0183 GGA, GSV and RMC sentences in below, in case it's useful to anyone. Admittedly the code is a bit rubbish, and not how I'd do it today. Sloppy code aside, the glaring issue is that I don't localise UTC time. I didn't include the RS232 code, because these days there are better solutions to that problem than mine. This MSDN page is a great place to start.

Anyway, click the little +/- signs to expand and collapse code blocks, and if you have any questions, contact me.

Expand/Collapse
#Region "ParseNMEA0183"
 
Private Sub ParseNMEA(ByVal nmeaSentence As String)

        Dim _CRPosition As Integer
        Dim _CurrentSentence As String

        If nmeaSentence.Length = 0 Then Exit Sub

        Try

            _CRPosition = nmeaSentence.IndexOf(vbCr, 0)

            Do While _CRPosition > 0

                _CurrentSentence = nmeaSentence.Substring(0, _CRPosition - 1)
                If _CRPosition < nmeaSentence.Length - 1 Then
                    nmeaSentence = nmeaSentence.Substring(_CRPosition + 2)
                    _CRPosition = nmeaSentence.IndexOf(vbCr)
                Else
                    _CRPosition = -1
                End If

                If _CurrentSentence.IndexOf("$GPGGA") >= 0 Then ParseGGA(_CurrentSentence)
                If _CurrentSentence.IndexOf("$GPGSV") >= 0 Then ParseGSV(_CurrentSentence)
                If _CurrentSentence.IndexOf("$GPRMC") >= 0 Then ParseRMC(_CurrentSentence)

            Loop

        Catch ex As Exception

            ' Ignore parse errors.

        End Try

    End Sub

#End Region
Expand/Collapse
#Region "ParseGGA"
 
    ' NMEA 0183 GGA sentences contain global positioning system fix data.
    '
    ' Seq.  Field                       Description
    ' 1     Sentence Id                 Talker ID and sentence ID: GP = GPS, GGA = fix data
    ' 2     UTC time                    UTC time at which fix was taken. Format: hhmmss.sss
    ' 3     Latitude                    Latitude ddmm.mmmm
    ' 4     Latitude hemisphere         Latitude hempisphere: N = North, S = South
    ' 5     Longitude                   Longitude: dddmm.mmmm
    ' 6     Longitude hempishere        Longitude hemisphere: E = East, W = West
    ' 7     Position fix                Fix quality: 0 = Invalid, 1 = Valid SPS, 2 = Valid DGPS, 3 = Valid PPS
    ' 8     Satellites used             Number of satellites being tracked (0-12)
    ' 9     HDOP                        Horizontal dilution of precision
    ' 10    Altitude                    Altitude above mean sea level according to WGS-84 ellipsoid
    ' 11    Altitude units              M = Meters
    ' 12    Geoid separation            Geoid separation above mean sea level according to WGS-84 ellipsoid
    ' 13    Geoid separation units      M = Meters
    ' 14    DGPS age                    Age of DGPS data in seconds (empty field)
    ' 15    DGPS station Id             DGPS station ID number (empty field)
    ' 16    Checksum                    Checksums are optional for most sentences
    '
    ' Samples:
    '   Signal not acquired:
    '       $GPGGA,235947.000,0000.0000,N,00000.0000,E,0,00,0.0,0.0,M,,,,0000*00
    '   Signal acquired:
    '       $GPGGA,092204.999,4250.5589,S,14718.5084,E,1,04,24.4,19.7,M,,,,0000*1F

    Private Sub ParseGGA(ByVal ggaSentence As String)

        Dim _Position As Integer = -1
        Dim _OldPosition As Integer = 0
        Dim _UnparsedSentence As String = ggaSentence
        Dim _CurrentSentence As String = String.Empty
        Dim _Item As String = String.Empty
        Dim _Delimiter As String = String.Empty

        Try

            For i As Integer = 1 To 15

                If i = 15 Then
                    _Delimiter = NMEA_CHECKSUMSTARTCHAR
                Else
                    _Delimiter = NMEA_VALUEDELIMITER
                End If

                _Position = _UnparsedSentence.IndexOf(_Delimiter, _OldPosition + 1)
                _CurrentSentence = _UnparsedSentence.Substring(_OldPosition + 1, _Position - _OldPosition - 1)

                If _CurrentSentence.Length > 0 Then
                    Select Case i
                        Case 2
                            _Item = "UTC Time"
                            Time_Label.Text = TimeSerial( _
                                                    Convert.ToInt32(_CurrentSentence.Substring(0, 2)), _
                                                    Convert.ToInt32(_CurrentSentence.Substring(2, 2)), _
                                                    Convert.ToInt32(_CurrentSentence.Substring(4, 2))).ToLongTimeString
                        Case 3
                            _Item = "Latitude"
                            Latitude_Label.Text = FormatNumber(ToDegrees(_CurrentSentence), 6)
                        Case 4
                            _Item = "Latitude Hemisphere"
                            Latitude_Label.Text += " " & _CurrentSentence
                        Case 5
                            _Item = "Longitude"
                            Longitude_Label.Text = FormatNumber(ToDegrees(_CurrentSentence), 6)
                        Case 6
                            _Item = "Longitude Hempsphere"
                            Longitude_Label.Text += " " & _CurrentSentence
                        Case 10
                            _Item = "Altitude"
                            Altitude_Label.Text = FormatNumber(Convert.ToSingle(_CurrentSentence), 2)
                    End Select
                End If

                _OldPosition = _Position

            Next

        Catch ex As Exception

            Throw New Exception("Error processing GGA. " & _Item & ": " & _CurrentSentence & vbCrLf & ex.Message)

        End Try

    End Sub

#End Region
Expand/Collapse
#Region "ParseGSV"
 
    ' NMEA 0183 GSV sentences contain data about satellites currently in view.
    '
    ' Seq.  Field                       Description
    ' 1     Sentence Id                 Talker ID: GP = GPS, Sentence ID: GSV = satellites in view
    ' 2     Number of messages          Number of messages in complete message (1 to 3)
    ' 3     Sequence number             Sequence number of this message (1 to 3)
    ' 4     Satellites in view          Total number of satellites currently in view
    ' 5     Satellite Id 1              Range is 1 to 32
    ' 6     Elevation 1                 Satellite elevation in degrees (0 to 90)
    ' 7     Azimuth 1                   Azimuth in degrees (0 to 359)
    ' 8     SNR 1                       Signal to noise ratio in dBHZ (0 to 99)
    ' 9     Satellite Id 2              Range is 1 to 32
    ' 10    Elevation 2                 Satellite elevation in degrees (0 to 90)
    ' 11    Azimuth 2                   Azimuth in degrees (0 to 359)
    ' 12    SNR 2                       Signal to noise ratio in dBHZ (0 to 99)
    ' 13    Satellite Id 3              Range is 1 to 32
    ' 14    Elevation 3                 Satellite elevation in degrees (0 to 90)
    ' 15    Azimuth 3                   Azimuth in degrees (0 to 359)
    ' 16    SNR 3                       Signal to noise ratio in dBHZ (0 to 99)
    ' 17    Satellite Id 4              Range is 1 to 32
    ' 18    Elevation 4                 Satellite elevation in degrees (0 to 90)
    ' 19    Azimuth 4                   Azimuth in degrees (0 to 359)
    ' 20    SNR 4                       Signal to noise ratio in dBHZ (0 to 99)
    ' 21    Checksum                    Checksums are optional for most sentences
    '
    ' Samples:
    '   Signal not acquired:
    '       $GPGGA,235947.000,0000.0000,N,00000.0000,E,0,00,0.0,0.0,M,,,,0000*00
    '   Signal acquired:
    '       $GPGGA,092204.999,4250.5589,S,14718.5084,E,1,04,24.4,19.7,M,,,,0000*1F

    Public Sub ParseGSV(ByVal gsvSentence As String)

        Dim _Position As Integer = -1
        Dim _OldPosition As Integer = 0
        Dim _UnparsedSentence As String = gsvSentence
        Dim _CurrentSentence As String = ""
        Dim _EndSent As Boolean = False
        Dim _Item As String = String.Empty
        Dim _Satellite As Satellite = Nothing
        Dim _SatsInView As Integer = 0

        Try

            ' GSV token
            _Item = "GSV Token"
            _Position = _UnparsedSentence.IndexOf(NMEA_VALUEDELIMITER, _OldPosition + 1)
            _CurrentSentence = _UnparsedSentence.Substring(_OldPosition + 1, _Position - _OldPosition - 1)
            _OldPosition = _Position

            ' total GSV messages in block
            _Item = "Total GSV Messages"
            _Position = _UnparsedSentence.IndexOf(NMEA_VALUEDELIMITER, _OldPosition + 1)
            _OldPosition = _Position

            ' current GSV message number
            _Item = "Current GSV Message"
            _Position = _UnparsedSentence.IndexOf(NMEA_VALUEDELIMITER, _OldPosition + 1)
            _OldPosition = _Position

            ' Sats in view
            _Item = "Satellites in View"
            _Position = _UnparsedSentence.IndexOf(NMEA_VALUEDELIMITER, _OldPosition + 1)
            _CurrentSentence = _UnparsedSentence.Substring(_OldPosition + 1, _Position - _OldPosition - 1)
            _SatsInView = Convert.ToInt32(_CurrentSentence)
            _OldPosition = _Position

            If _SatsInView > 0 Then

                ' Up to 4 sats
                For i As Integer = 1 To 4

                    _Satellite = New Satellite

                    _Item = "Satellite ID"
                    _Position = _UnparsedSentence.IndexOf(NMEA_VALUEDELIMITER, _OldPosition + 1)
                    _CurrentSentence = _UnparsedSentence.Substring(_OldPosition + 1, _Position - _OldPosition - 1)
                    _Satellite.ID = Convert.ToInt32(_CurrentSentence)
                    _OldPosition = _Position

                    _Item = "Elevation"
                    _Position = _UnparsedSentence.IndexOf(NMEA_VALUEDELIMITER, _OldPosition + 1)
                    _CurrentSentence = _UnparsedSentence.Substring(_OldPosition + 1, _Position - _OldPosition - 1)
                    If _CurrentSentence.Length > 0 Then
                        _Satellite.Elevation = Convert.ToInt32(_CurrentSentence)
                    Else
                        _Satellite.Elevation = 0
                    End If
                    _OldPosition = _Position

                    _Item = "Azimuth"
                    _Position = _UnparsedSentence.IndexOf(NMEA_VALUEDELIMITER, _OldPosition + 1)
                    _CurrentSentence = _UnparsedSentence.Substring(_OldPosition + 1, _Position - _OldPosition - 1)
                    If _CurrentSentence.Length > 0 Then
                        _Satellite.Azimuth = Convert.ToInt32(_CurrentSentence)
                    Else
                        _Satellite.Azimuth = 0
                    End If
                    _OldPosition = _Position

                    _Item = "Signal to Noise Ratio"
                    _Position = _UnparsedSentence.IndexOf(NMEA_VALUEDELIMITER, _OldPosition + 1)
                    If _Position = -1 Then
                        _Satellite.SNR = 0
                        _EndSent = True
                    Else
                        _CurrentSentence = _UnparsedSentence.Substring(_OldPosition + 1, _Position - _OldPosition - 1)
                        If _CurrentSentence.Length > 0 Then
                            _Satellite.SNR = Convert.ToInt32(_CurrentSentence)
                        Else
                            _Satellite.SNR = 0
                        End If
                        _OldPosition = _Position
                    End If

                    If m_Satellites.IndexOf(_Satellite.ID) >= 0 Then
                        m_Satellites(m_Satellites.IndexOf(_Satellite.ID)) = _Satellite
                    ElseIf m_Satellites.Count >= 12 Then
                        Dim _OldestSatIndex As Integer
                        Dim _OldestCreateTime As Date = Now
                        For j As Integer = 0 To m_Satellites.Count - 1
                            If m_Satellites(j).Timestamp < _OldestCreateTime Then
                                _OldestSatIndex = j
                                _OldestCreateTime = m_Satellites(j).Timestamp
                            End If
                        Next
                        m_Satellites(_OldestSatIndex) = _Satellite
                    Else
                        m_Satellites.Add(_Satellite)
                    End If

                    DrawSatLocation()   ' draw satellite location on horizon
                    DrawSatStrength()   ' draw satellite signal to noise ratio

                    If _EndSent Then Exit For

                Next

            End If

        Catch ex As Exception

            Throw New Exception("Error processing GSV. " & _Item & ": " & _CurrentSentence & vbCrLf & ex.Message)

        End Try

    End Sub

#End Region
Expand/Collapse
#Region "ParseRMC"
 
    ' NMEA 0183 RMC sentences contain recommended minimum specific GPS data.
    '
    ' Seq.  Field                       Description
    ' 1     Sentence ID                 Talker ID: GP = GPS, Sentence ID: RMC = GPS/transit date
    ' 2     UTC Time                    UTC time at which fix was taken. Format: hhmmss.sss
    ' 3     Status                      Status: A = Valid, V = Invalid
    ' 4     Latitude                    Latitude ddmm.mmmm
    ' 5     Latitude Hemisphere         Latitude hemisphere: N = North, S = South
    ' 6     Longitude                   Longitude: dddmm.mmmm
    ' 7     Longitude Hemisphere        Longitude hemisphere: E = East, W = West
    ' 8     Speed over ground           Speed over ground, measured in knots
    ' 9     Course over ground          Course over ground, in degrees (0 to 359.9)
    ' 10    UTC Date                    UTC Date in the format DDMMYY
    ' 11    Magnetic Variation          Magnetic variation measured in degrees
    '       Mag. Variation Hemisphere   Magnetic variation hemisphere: E = East, W = West
    ' 12    Checksum                    Checksums are optional for most sentences
    '
    ' Samples:
    '   Signal not acquired:
    '       $GPRMC,235947.000,V,0000.0000,N,00000.0000,E,,,041299,,*1D
    '   Signal acquired:
    '       $GPRMC,092204.999,A,4250.5589,S,14718.5084,E,0.00,89.68,211200,,*25

    Public Sub ParseRMC(ByVal rmcSentence As String)

        Dim _Position As Integer = -1
        Dim _OldPosition As Integer = 0
        Dim _UnparsedSentence As String = rmcSentence
        Dim _CurrentSentence As String = String.Empty
        Dim _Delimiter As String = String.Empty
        Dim _Item As String = String.Empty

        Try

            For i As Integer = 1 To 12

                If i = 12 Then
                    _Delimiter = NMEA_CHECKSUMSTARTCHAR
                Else
                    _Delimiter = NMEA_VALUEDELIMITER
                End If

                _Position = _UnparsedSentence.IndexOf(_Delimiter, _OldPosition + 1)
                _CurrentSentence = _UnparsedSentence.Substring(_OldPosition + 1, _Position - _OldPosition - 1)

                If _CurrentSentence.Length > 0 Then
                    Select Case i
                        Case 2
                            _Item = "UTC Time"
                            Time_Label.Text = TimeSerial( _
                                                    Convert.ToInt32(_CurrentSentence.Substring(0, 2)), _
                                                    Convert.ToInt32(_CurrentSentence.Substring(2, 2)), _
                                                    Convert.ToInt32(_CurrentSentence.Substring(4, 2))).ToLongTimeString
                        Case 3
                            _Item = "Status"
                            Select Case _CurrentSentence
                                Case "V"
                                    Status_Label.Text = "Awaiting position fix..."
                                Case "A"
                                    Status_Label.Text = "Valid position fix"
                                Case Else
                                    Status_Label.Text = "Invalid position fix"
                            End Select
                        Case 4
                            _Item = "Latitude"
                            Latitude_Label.Text = FormatNumber(ToDegrees(_CurrentSentence), 6)
                        Case 5
                            _Item = "Latitude Hemisphere"
                            Latitude_Label.Text += " " & _CurrentSentence
                        Case 6
                            _Item = "Longitude"
                            Longitude_Label.Text = FormatNumber(ToDegrees(_CurrentSentence), 6)
                        Case 7
                            _Item = "Longitude Hemisphere"
                            Longitude_Label.Text += " " & _CurrentSentence
                        Case 8
                            _Item = "Speed"
                            Speed_Label.Text = FormatNumber(Convert.ToSingle(_CurrentSentence), 2)
                        Case 9
                            _Item = "Heading"
                            Bearing_Label.Text = FormatNumber(Convert.ToSingle(_CurrentSentence), 2)
                            If Convert.ToSingle(_CurrentSentence) <> m_Bearing Then
                                m_Bearing = Convert.ToSingle(_CurrentSentence)
                                DrawSatLocation()   ' Re-draw bearing bug.
                            End If
                        Case 10
                            _Item = "UTC Date"
                            Date_Label.Text = DateSerial( _
                                                    Convert.ToInt32(_CurrentSentence.Substring(4, 2)), _
                                                    Convert.ToInt32(_CurrentSentence.Substring(2, 2)), _
                                                    Convert.ToInt32(_CurrentSentence.Substring(0, 2))).ToShortDateString
                    End Select
                End If

                _OldPosition = _Position

            Next

        Catch ex As Exception

            Throw New Exception("Error processing RMC. " & _Item & ": " & _CurrentSentence & vbCrLf & ex.Message)

        End Try

    End Sub

#End Region

ParseGSV() uses an array (I know. WTF?) of Satellites. This is the Satellite class:

Expand/Collapse
#Region "Satellite"
 
Option Explicit On 
Option Strict On

Public Class Satellite

    ' Private instance attributes

    Private m_SatelliteId As Integer = 0
    Private m_Elevation As Integer = 0
    Private m_Azimuth As Integer = 0
    Private m_SNR As Integer = 0
    Private m_Timestamp As Date = Date.MinValue()

    ' Public instance attributes

    Public Property Id() As Integer
        Get
            Return m_SatelliteId
        End Get
        Set(ByVal Value As Integer)
            m_SatelliteId = Value
        End Set
    End Property

    Public Property Elevation() As Integer
        Get
            Return m_Elevation
        End Get
        Set(ByVal Value As Integer)
            m_Elevation = Value
        End Set
    End Property

    Public Property Azimuth() As Integer
        Get
            Return m_Azimuth
        End Get
        Set(ByVal Value As Integer)
            m_Azimuth = Value
        End Set
    End Property

    Public Property SNR() As Integer
        Get
            Return m_SNR
        End Get
        Set(ByVal Value As Integer)
            m_SNR = Value
        End Set
    End Property

    Public ReadOnly Property Timestamp() As Date
        Get
            Return m_Timestamp
        End Get
    End Property

    ' Constructors

    Public Sub New()
        MyBase.new()
        m_Timestamp = Now
    End Sub

End Class

#End Region

In conclusion, what's amusing is contrasting the Pocket PC app with the first Windows Phone app I wrote in early 2011:

Compass rose
Compass rose

Home | Blog | Photos | Contact | About

Wittenburg.co.uk and all content copyright 1995-2018 by Michael Wittenburg, unless otherwise stated.
All content on this site is licensed under the Creative Commons license, unless otherwise stated.

Wittenburg.co.uk uses a single session cookie because it's required by the tech underlying the site (Microsoft ASP.NET). The cookie stores no information and seves no functional purpose.