65.9K
CodeProject 正在变化。 阅读更多。
Home

编写自己的 GPS 应用程序:第一部分

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.57/5 (788投票s)

2004 年 8 月 29 日

CPOL

10分钟阅读

viewsIcon

1096858

downloadIcon

5856

GPS 应用程序需要具备怎样的功能才能满足车载导航的需求?此外,解析 GPS 数据实际是如何工作的?在本系列的三部分文章中,我将涵盖这两个主题,并赋予你编写商业级 GPS 应用程序所需的技能。

“我一直对我们头顶 11,000 英里高空的卫星能够如此轻松地被用于原子钟感到惊叹。”

Sample Image - WritingGPSApplications1.jpg

引言

GPS 应用程序需要具备怎样的功能才能满足商业环境(例如车载导航)的需求?此外,解析 GPS 数据实际是如何工作的?在本系列的三部分文章中,我将涵盖这两个主题,并赋予你编写符合当今行业大多数 GPS 设备要求的商业级 GPS 应用程序所需的技能。

一句强有力的话

本系列的第一部分将探讨解析原始 GPS 数据的任务。幸运的是,这项任务因为美国国家海洋电子协会 (National Marine Electronics Association) 的推出而变得简单,该协会为行业引入了一个标准,目前已被绝大多数 GPS 设备采用。为了给开发者一个良好的开端,我选择使用我“GPS.NET 全球定位 SDK”组件中的一些 Visual Studio .NET 源代码。(为简洁起见,代码已移除多线程和错误处理等功能。)

NMEA 数据以逗号分隔的“句子”形式发送,其中包含基于句子第一个单词的信息。有超过五十种句子类型,但解析器实际上只需要处理其中的几种即可完成工作。所有 NMEA 句子中最常见的是“推荐最小”句子,它以“$GPRMC”开头。示例如下:

$GPRMC,040302.663,A,3939.7,N,10506.6,W,0.27,358.86,200804,,*1A

这个句子几乎包含了 GPS 应用程序所需的所有信息:纬度、经度、速度、航向、卫星时间、定位状态和磁偏角。

解析器的核心

创建 NMEA 解析器的第一步是编写一个方法,该方法执行两项操作:将每个句子分解成独立的单词,并检查第一个单词以确定可提取的信息。清单 1-1 显示了解析器类的开头。

(清单 1-1:NMEA 解析器的核心是一个将 NMEA 句子分解成独立单词的函数。)
'*******************************************************

'**  Listing 1-1.  The core of an NMEA interpreter

'*******************************************************

Public Class NmeaInterpreter
  ' Processes information from the GPS receiver
  Public Function Parse(ByVal sentence As String) As Boolean
    ' Divide the sentence into words
    Dim Words() As String = GetWords(sentence)
    ' Look at the first word to decide where to go next
    Select Case Words(0)
      Case "$GPRMC"      ' A "Recommended Minimum" sentence was found!
        ' Indicate that the sentence was recognized
        Return True
      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
End Class

下一步是执行实际的信息提取,首先是纬度和经度。纬度和经度以“DDD°MM’SS.S”的形式存储,其中 D 代表度(也称为“小时”),M 代表分,S 代表秒。坐标可以以简写形式显示,例如“DD°MM.M’”或“DD°”。句子中的第四个单词“3939.7”显示当前纬度为度和分(39°39.7’),只是数字被挤在了一起。前两个字符(39)代表度,其余部分(39.7)代表分。经度的结构相同,只是前三个字符代表度(105°06.6’)。第五个和第七个单词指示“半球”,其中“N”代表“北”,“W”代表“西”等。半球附加到数字部分的末尾,构成完整的测量值。我发现 NMEA 解析器作为事件驱动的,更容易使用。这是因为数据到达的顺序是不确定的。事件驱动类为应用程序提供了最大的灵活性和响应能力。因此,我将设计解析器使用事件来报告信息。第一个事件 `PositionReceived` 将在接收到当前纬度和经度时触发。清单 1-2 扩展了解析器以报告当前位置。

(清单 1-2:解析器现在可以报告当前的纬度和经度。)
'*******************************************************

'**  Listing 1-2.  Extracting information from a sentence

'*******************************************************

Public Class NmeaInterpreter
  ' Raised when the current location has changed
  Public Event PositionReceived(ByVal latitude As String, _
                                ByVal longitude As String)
  ' Processes information from the GPS receiver
  Public Function Parse(ByVal sentence As String) As Boolean
    ' 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 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
      Dim Latitude As String = Words(3).Substring(0, 2) & "°" ' Append hours
      Latitude = Latitude & Words(3).Substring(2) & """"      ' Append minutes
      Latitude = Latitude & Words(4)     ' Append the hemisphere
      Dim Longitude As String = Words(5).Substring(0, 3) & "°" ' Append hours
      Longitude = Longitude & Words(5).Substring(3) & """"     ' Append minutes
      Longitude = Longitude & Words(6)     ' Append the hemisphere
      ' Notify the calling application of the change
      RaiseEvent PositionReceived(Latitude, Longitude)
    End If
    ' Indicate that the sentence was recognized
    Return True
  End Function
End Class

需要注意的一点是,某些 GPS 设备在信息未知时会报告空白值。因此,在解析之前测试每个单词是否具有值是个好主意。如果需要输入度符号(°),请按住 Alt 键并在数字小键盘上输入“0176”。

清除垃圾

校验和是通过对美元符号和星号之间的字节(不包括它们)进行异或运算来计算的。然后将此校验和与句子中的校验和进行比较。如果校验和不匹配,句子通常会被丢弃。这样做是没问题的,因为 GPS 设备倾向于每隔几秒重复相同的信息。通过比较校验和,解析器能够丢弃任何校验和无效的句子。清单 1-3 扩展了解析器以执行此操作。

(清单 1-3:解析器现在可以检测错误并仅解析无错误 NMEA 数据。)
'*******************************************************

'**  Listing 1-3.  Detecting and handling NMEA errors

'*******************************************************

Public Class NmeaInterpreter
  ' Raised when the current location has changed
  Public Event PositionReceived(ByVal latitude As String, _
                                ByVal longitude As String)
  ' 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 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
      Dim Latitude As String = Words(3).Substring(0, 2) & "°" ' Append hours
      Latitude = Latitude & Words(3).Substring(2) & """"      ' Append minutes
      Latitude = Latitude & Words(4)     ' Append the hemisphere
      Dim Longitude As String = Words(5).Substring(0, 3) & "°" ' Append hours
      Longitude = Longitude & Words(5).Substring(3) & """"     ' Append minutes
      Longitude = Longitude & Words(6)     ' Append the hemisphere
      ' Notify the calling application of the change
      RaiseEvent PositionReceived(Latitude, Longitude)
    End If
    ' Indicate that the sentence was recognized
    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

无线原子时间

时间是 GPS 技术的基石,因为距离是在光速下测量的。每颗 GPS 卫星都包含四个原子钟,用于在纳秒级别计时其无线电传输。一个引人入胜的特点是,只需几行代码,就可以利用这些原子钟以毫秒精度同步计算机的时钟。 `$GPRMC` 句子的第二个单词“040302.663”包含压缩格式的卫星时间。前两个字符代表小时,接下来的两个字符代表分钟,再接下来的两个字符代表秒,小数点之后的所有内容都是毫秒。因此,时间是凌晨 4:03:02.663。但是,卫星报告的是世界协调时间 (GMT+0),因此必须将时间调整为本地时区。清单 1-4 增加了对卫星时间的支持,并使用 `DateTime.ToLocalTime` 方法将卫星时间转换为本地时区。

(清单 1-4:该类现在可以使用原子钟无线同步您的计算机时钟。)
'********************************************************

'**  Listing 1-4.  Add support for satellite-derived time

'********************************************************

Public Class NmeaInterpreter
  ' 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)
  ' 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 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
      Dim Latitude As String = Words(3).Substring(0, 2) & "°" ' Append hours
      Latitude = Latitude & Words(3).Substring(2) & """"    ' Append minutes
      Latitude = Latitude & Words(4)     ' Append the hemisphere
      Dim Longitude As String = Words(5).Substring(0, 3) & "°" ' Append hours
      Longitude = Longitude & Words(5).Substring(3) & """"    ' Append minutes
      Longitude = Longitude & Words(6)     ' Append the hemisphere
      ' 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(Single.Parse(Words(1).Substring(6), _
              CultureInfo.InvariantCulture) * 1000, Integer)
      End If
      ' 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
    ' Indicate that the sentence was recognized
    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

方向和速度警报

GPS 设备会随时间分析您的位置来计算速度和航向。本文开头提到的 `$GPRMC` 句子也包含这些读数。速度始终以节为单位报告,航向报告为“方位角”,围绕地平线测量的角度,从 0° 到 360° 顺时针测量,其中 0° 代表北方,90° 代表东方,以此类推。需要进行一些数学计算才能将节转换为英里/小时。清单 1-5 中的一行代码再次展示了 GPS 的强大功能,该代码可以确定汽车是否超速。

(清单 1-5:该类现在可以告诉您前进的方向,并帮助您避免超速罚单。)
'*******************************************************

'**  Listing 1-5.  Extracting speed and bearing

'*******************************************************

Public Class NmeaInterpreter
  ' 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()
  ' 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 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
      Dim Latitude As String = Words(3).Substring(0, 2) & "°" ' Append hours
      Latitude = Latitude & Words(3).Substring(2) & """"    ' Append minutes
      Latitude = Latitude & Words(4)     ' Append the hemisphere
      Dim Longitude As String = Words(5).Substring(0, 3) & "°" ' Append hours
      Longitude = Longitude & Words(5).Substring(3) & """"    ' Append minutes
      Longitude = Longitude & Words(6)     ' Append the hemisphere
      ' 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(Single.Parse(Words(1).Substring(6), _
                CultureInfo.InvariantCulture) * 1000, 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.  Convert it into MPH
      Dim Speed As Double = CType(Words(7), Double) * 1.150779
      ' If we're over 55MPH then trigger a speed alarm!
      If Speed > 55 Then RaiseEvent SpeedLimitReached()
      ' Notify of the new speed
      RaiseEvent SpeedReceived(Speed)
    End If
    ' Do we have enough information to extract bearing?
    If Words(8) <> "" Then
      ' Indicate that the sentence was recognized
      Dim Bearing As Double = CType(Words(8), Double)
      RaiseEvent BearingReceived(Bearing)
    End If
    ' Indicate that the sentence was recognized
    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

我们定位了吗?

`$GPRMC` 句子包含一个值,指示是否已获得“定位”。当至少三颗卫星的信号强度足以计算您的位置时,就可以获得定位。如果涉及至少四颗卫星,还可以知道高度。`$GPRMC` 句子的第三个单词是两个字母之一:“A”代表“活动”,表示已获得定位;“V”代表“无效”,表示未获得定位。清单 1-6 包含检查此字符并报告定位状态的代码。

(清单 1-6:解析器现在知道设备何时已获得定位。)
'*******************************************************

'**  Listing 1-6.  Extracting satellite fix status

'*******************************************************

Public Class NmeaInterpreter

  ' 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()
  ' 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 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
      Dim Latitude As String = Words(3).Substring(0, 2) & "°"  ' Append hours
      Latitude = Latitude & Words(3).Substring(2) & """"    ' Append minutes
      Latitude = Latitude & Words(4)     ' Append the hemisphere
      Dim Longitude As String = Words(5).Substring(0, 3) & "°"  ' Append hours
      Longitude = Longitude & Words(5).Substring(3) & """"    ' Append minutes
      Longitude = Longitude & Words(6)     ' Append the hemisphere
      ' 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(Single.Parse(Words(1).Substring(6), _
                     CultureInfo.InvariantCulture) * 1000, 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.  Convert it into MPH
      Dim Speed As Double = CType(Words(7), Double) * 1.150779
      ' If we're over 55MPH then trigger a speed alarm!
      If Speed > 55 Then RaiseEvent SpeedLimitReached()
      ' Notify of the new speed
      RaiseEvent SpeedReceived(Speed)
    End If
    ' Do we have enough information to extract bearing?
    If Words(8) <> "" Then
      ' Indicate that the sentence was recognized
      Dim Bearing As Double = CType(Words(8), Double)
      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
  ' 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

如您所见,单个 NMEA 句子中 packed 了大量信息。现在 `$GPRMC` 句子已完全解析,可以将解析器扩展为支持第二个句子:`$GPGSV`。这个句子实时描述了上方卫星的配置。

实时卫星跟踪

了解卫星位置对于确定读数的精度以及 GPS 定位的稳定性很重要。由于 GPS 精度将在本系列的第二部分中详细介绍,因此本节将重点关注解析卫星位置和信号强度。轨道上有二十四颗运行中的卫星。卫星在轨道上的间隔很均匀,因此任何时候用户在世界任何地方都可以看到至少六颗卫星。卫星不断运动,这很好,因为它避免了“盲区”的存在,即世界各地卫星可见性很低或根本不可见的地方。就像在天空中寻找星星一样,卫星位置由方位角和仰角组合描述。如上所述,方位角测量地平线周围的方向。仰角是从地平线向上测量的角度,范围在 0° 到 90° 之间,其中 0° 代表地平线,90° 代表“天顶”,即正上方。因此,如果设备显示卫星的方位角为 45°,仰角为 45°,则该卫星位于地平线上方一半朝向东北方向。除了位置,设备还会报告每颗卫星的“伪随机码”(或 PRC),这是一个用于唯一标识一颗卫星与另一颗卫星的数字。这是一个 `$GPGSV` 句子的示例:

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

每个句子包含最多四个卫星信息块,由四个单词组成。例如,第一个块是“24,82,023,40”,第二个块是“05,62,285,32”,依此类推。每个块的第一个单词给出卫星的 PRC。第二个单词给出每颗卫星的仰角,然后是方位角和信号强度。如果将此卫星信息以图形方式显示,效果将如图 1-1 所示:

(图 1-1:`$GPGSV` 句子的图形表示,其中圆的中心表示当前位置,圆的边缘表示地平线。)

也许,这个句子中最重要数字是“信噪比”(简称 SNR)。这个数字表示卫星无线电信号的接收强度。请记住,卫星以相同的强度传输信号,但树木和墙壁等物体可能会使信号无法识别。典型的 SNR 值在零到五十之间,五十表示信号极佳。(SNR 可能高达九十九,但我从未在开阔的天空中见过高于五十的读数。)在图 1-1 中,绿色卫星表示信号强,而黄色卫星表示信号中等(在本系列的第二部分中,我将提供一种对信号强度进行分类的方法)。卫星 #1 的信号完全被遮挡。清单 1-7 显示了解析器在扩展以读取卫星信息后的样子。

(清单 1-7:解析器得到了改进,可以解析当前可见的 GPS 卫星的位置。)
'*******************************************************

'**  Listing 1-7.  Extracting satellite information

'*******************************************************

Public Class NmeaInterpreter

  ' 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)
  ' 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" message was found
        Return ParseGPGSV(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
      Dim Latitude As String = Words(3).Substring(0, 2) & "°"  ' Append hours
      Latitude = Latitude & Words(3).Substring(2) & """"    ' Append minutes
      Latitude = Latitude & Words(4)     ' Append the hemisphere
      Dim Longitude As String = Words(5).Substring(0, 3) & "°" ' Append hours
      Longitude = Longitude & Words(5).Substring(3) & """"    ' Append minutes
      Longitude = Longitude & Words(6)     ' Append the hemisphere
      ' 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(Single.Parse(Words(1).Substring(6), _
                     CultureInfo.InvariantCulture) * 1000, 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.  Convert it into MPH
      Dim Speed As Double = CType(Words(7), Double) * 1.150779
      ' If we're over 55MPH then trigger a speed alarm!
      If Speed > 55 Then RaiseEvent SpeedLimitReached()
      ' Notify of the new speed
      RaiseEvent SpeedReceived(Speed)
    End If
    ' Do we have enough information to extract bearing?
    If Words(8) <> "" Then
      ' Indicate that the sentence was recognized
      Dim Bearing As Double = CType(Words(8), Double)
      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
  ' 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

世界一流的解析器

国际读者可能早就注意到列表中未处理的一个微妙问题——数字是以美国使用的数字格式报告的!像比利时和瑞士这样的国家使用不同的数字格式,这需要对解析器进行调整才能正常工作。幸运的是,.NET Framework 内置了支持,用于在不同文化之间转换数字,因此对解析器进行的更改很简单。在解析器中,唯一的小数值是速度,因此只需要一项更改。`NmeaCultureInfo` 变量代表 NMEA 句子中使用的数字格式。然后使用此变量调用 `Double.Parse` 方法将速度转换为机器的本地格式。清单 1-8 显示了完整的解析器,现已准备好在全球范围内使用。

(清单 1-8:完整的解析器,适用于世界任何地方。)
'*************************************************************

'**  Listing 1-8.  Adding support for international cultures

'*************************************************************

Imports System.Globalization
Public Class NmeaInterpreter

  ' Represents the EN-US culture, used for numbers in NMEA sentences
  Private NmeaCultureInfo As New CultureInfo("en-US")
  ' Used to convert knots into miles per hour
  Private 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)
  ' 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"
        Return ParseGPGSV(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
      Dim Latitude As String = Words(3).Substring(0, 2) & "°"  ' Append hours
      Latitude = Latitude & Words(3).Substring(2) & """"    ' Append minutes
      Latitude = Latitude & Words(4)     ' Append the hemisphere
      Dim Longitude As String = Words(5).Substring(0, 3) & "°" ' Append hours
      Longitude = Longitude & Words(5).Substring(3) & """"    ' Append minutes
      Longitude = Longitude & Words(6)     ' Append the hemisphere
      ' 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(Single.Parse(Words(1).Substring(6), _
                      CultureInfo.InvariantCulture) * 1000, Integer)
      End If
      ' 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 = CType(Words(8), Double)
      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
  ' 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

最终想法

现在您应该对 NMEA 解析器就是提取句子中的单词有了很好的理解。您可以利用卫星的力量来确定您的位置,同步您的计算机时钟,查找方向,监控速度,并在阴天时指向天空中的卫星。此解析器无需任何修改即可与 .NET Compact Framework 一起使用。如果还将句子存储在文件中,则可以使用解析器回放整个公路旅行。这些都是很棒的功能,尤其是考虑到该类的规模很小,但这个解析器是否已准备好驾驶您的汽车和驾驶飞机?还不能完全。还有一个重要主题需要考虑,才能使 GPS 应用程序在现实世界中安全可靠:精度。GPS 设备旨在报告它们找到的任何信息,即使该信息不准确。事实上,即使设备配备了最新的 DGPS 和 WAAS 校正技术,当前位置的信息也可能相差半个足球场!不幸的是,一些开发者并没有意识到这个问题。市面上有一些第三方组件不适合需要强制执行最低精度级别的商业应用程序。不过,请保留本文,因为在本系列的第二部分中,我将详细解释精度强制执行,并将解析器进一步改进,使其适用于专业、高精度应用程序!

© . All rights reserved.