提供GPS功能的Wince和Windows Mobile都需要一个GPS接收器(GPS Receiver)。GPS receiver就像一个收音机,他从太空中各个GPS卫星(Satellites)接收信号,通过自身的算法(一般在Firmware里面)计算出位置等信息,然后以NMEA data的格式输出。GPS receiver就是接收卫星信号转换成NMEA data的设备。
进行GPS的开发需要从GPS receiver取出NMEA data,分析出关心的数据。关心的数据包括经度(Longitude),维度(Latitude)和海拔(Altitude)等等。在Windows Mobile 5以上MS提供了GPS Intermediate Driver,开发人员不再需要自己分析NMEA data了。但是Wince5以及以下版本不提供GPS Intermediate Driver,还是需要自己分析NMEA data来取出关心的信息。本文讲述如何使用C#进行NMEA data的分析。第一眼看,分析NMEA有自己做轮子之嫌,其实了解NMEA的分析也是有好处的,由于各个生产GPS receiver的厂商在硬件工艺和算法的不一样,各个厂商都提供自己扩展的NMEA data,这些数据GPS Intermediate Driver是不支持的,需要自己分析。
NMEA 全称NMEA 0183,是电子与数据的通信规范,也就是协议。实现该协议的设备输出这种规范的数据,其他应用就可以基于这协议分析出相关的数据。NMEA开始用在航海设备上,现在广泛用在GPS设备上,这就是为什么NMEA的原始速度使用Knot(海里/小时)表示。下面是一段GPS NMEA data的范例
$GPRMC, 000006 ,A, 3754.6240 ,S, 14509.7720 ,E, 010.8 , 313.1 , 010108 , 011.8 ,E * 6A$GPGGA, 201033 , 3754.6240 ,S, 14509.7720 ,E, 1 , 05 , 1.7 , 91.1 ,M, - 1.1 ,M,, * 75 $GPGSA,A, 3 ,, 05 , 10 ,,,, 21 ,, 29 , 30 ,,, 2.9 , 1.7 , 1.3 * 32 $GPGSV, 3 , 3 , 12 , 29 , 74 , 163 , 41 , 30 , 53 , 337 , 40 , 31 , 09 , 266 , 00 , 37 , 00 , 000 , 00 * 78 $PGRME, 6.3 ,M, 11.9 ,M, 13.5 ,M * 25 $PGRMB, 0.0 , 200 ,,,,K,,N,W * 28 $PGRMM,WGS 84 * 06
GPS NMEA data有以下特点:
* 每一条NMEA data的数据都是以dollar符号开头。
* 从第二个字符开始的前2个字符表示发送者(talker)和接着3个字符表示数据(message)。其中上面的talker中,GP表示通用的GPS NMEA data,而PG为特定厂商的NMEA data。
* 所有数据字段(data fields)都是使用逗号隔开(comma-delimited)。
* 最后一个数据段接着一个星号(asterisk)。
* 星号后面是两位数字的校正码(checksum),checksum的计算方法是或计算在 '$' 和 '*'之间的所有字符。
* 最后以回车换行(<CR><LF>)结尾。
有了上述规范,开发NMEA的分析器就变得十分简单,分析流程是:先接收一条NMEA语句(NMEA sentence),然后检查语句格式,检查checksum,然后再根据talker和message进行分发,使用不同的算法进行分析。下面为核心分析流程。
public bool Parse( string sentence) { string rawData = sentence; try { if ( ! IsValid(sentence)) { return false ; } sentence = sentence.Substring( 1 , sentence.IndexOf( ' * ' ) - 1 ); string [] Words = Getwords(sentence); switch (Words[ 0 ]) { case " GPRMC " : return ParseGPRMC(Words); case " GPGGA " : return ParseGPGGA(Words); case " GPGSA " : return ParseGPGSA(Words); case " GPGSV " : return ParseGPGSV(Words); default : return false ; } } catch (Exception e) { Console.WriteLine(e.Message + rawData); return false ; } } 代码1
Parse为分析接口,所有从GPS Receiver接收到NMEA data全部调用这个接口进行分析。
IsValid检验该NMEA sentence是否有效。
Checksum进行Checksum运算,检验校验码是否有效。
接下来,讲述关键语句的分析。进行语句的分析,需要一个NMEA的规范手册,这个手册可以从GPS厂商下载,例如从Garmin下载
在该手册的第24页可以看到GPRMC的协议定义。
图1
根据手册的规范定义,抽取想要的信息。如下代码:
private bool ParseGPRMC( string [] Words) { if (Words[ 1 ].Length > 0 & Words[ 9 ].Length > 0 ) { int UtcHours = Convert.ToInt32(Words[ 1 ].Substring( 0 , 2 )); int UtcMinutes = Convert.ToInt32(Words[ 1 ].Substring( 2 , 2 )); int UtcSeconds = Convert.ToInt32(Words[ 1 ].Substring( 4 , 2 )); int UtcMilliseconds = 0 ; // Extract milliseconds if it is available if (Words[ 1 ].Length > 7 ) { UtcMilliseconds = Convert.ToInt32(Words[ 1 ].Substring( 7 )); } int UtcDay = Convert.ToInt32(Words[ 9 ].Substring( 0 , 2 )); int UtcMonth = Convert.ToInt32(Words[ 9 ].Substring( 2 , 2 )); // available for this century int UtcYear = Convert.ToInt32(Words[ 9 ].Substring( 4 , 2 )) + 2000 ; utcDateTime = new DateTime(UtcYear, UtcMonth, UtcDay, UtcHours, UtcMinutes, UtcSeconds, UtcMilliseconds); } fixStatus = (Words[ 2 ][ 0 ] == ' A ' ) ? FixStatus.Obtained : FixStatus.Lost; if (Words[ 3 ].Length > 0 & Words[ 4 ].Length == 1 & Words[ 5 ].Length > 0 & Words[ 6 ].Length == 1 ) { latitude.Hours = int .Parse(Words[ 3 ].Substring( 0 , 2 )); latitude.Minutes = int .Parse(Words[ 3 ].Substring( 2 , 2 )); latitude.Seconds = Math.Round( double .Parse(Words[ 3 ].Substring( 5 , 4 )) * 6 / 1000.0 , 3 ); if ( " S " == Words[ 4 ]) { latitude.Hours = - latitude.Hours; } longitude.Hours = int .Parse(Words[ 5 ].Substring( 0 , 3 )); longitude.Minutes = int .Parse(Words[ 5 ].Substring( 3 , 2 )); longitude.Seconds = Math.Round( double .Parse(Words[ 5 ].Substring( 6 , 4 )) * 6 / 1000.0 , 3 ); if ( " W " == Words[ 6 ]) { longitude.Hours = - longitude.Hours; } } if (Words[ 8 ].Length > 0 ) { azimuth = decimal .Parse(Words[ 8 ], NmeaCultureInfo); } if (Words[ 7 ].Length > 0 ) { velocity = decimal .Parse(Words[ 7 ], NmeaCultureInfo) * KMpHPerKnot; } return true ; } 代码2
从ParseGPRMC看,传递的参数是一个string的数组,分析要做的事情就是把数组的元素根据手册翻译成需要的信息,例如字段1为UTC的日期信息,字段9为UTC的时间信息。字段2为fix信息,就是GPS是否完成了初始化的信息。字段3,4为经度,字段5,6为维度。字段7为速度。字段8为角度。
private bool ParseGPGGA( string [] Words) { if (Words[ 6 ].Length > 0 ) { switch (Convert.ToInt32(Words[ 6 ])) { case 0 : differentialGpsType = DifferentialGpsType.NotSet; break ; case 1 : differentialGpsType = DifferentialGpsType.SPS; break ; case 2 : differentialGpsType = DifferentialGpsType.DSPS; break ; case 3 : differentialGpsType = DifferentialGpsType.PPS; break ; case 4 : differentialGpsType = DifferentialGpsType.RTK; break ; default : differentialGpsType = DifferentialGpsType.NotSet; break ; } } if (Words[ 7 ].Length > 0 ) { satellitesInUsed = Convert.ToInt32(Words[ 7 ]); } if (Words[ 8 ].Length > 0 ) { horizontalDilutionOfPrecision = Convert.ToDecimal(Words[ 8 ]); } if (Words[ 9 ].Length > 0 ) { altitude = Convert.ToDecimal(Words[ 9 ]); } return true ; } 代码3
分析ParseGPGGA和分析ParseGPRMC一样,从数组抽取信息,字段6为fix类型,这个参数表示使用了那些辅佐卫星或者地面信号站来提高GPS的精度。SPS为普通类型,DSPS使用了DGPS 地面信号站fix,DSPS使用了WAAS位置卫星fix(只是用在美国),PPS使用了EGNOS位置卫星fix(只是用在欧洲),RTK使用了MSAS位置卫星fix(只是用在亚洲)。字段7为使用卫星的数量。字段8为水平精度。字段9为海拔。
private bool ParseGPGSA( string [] Words) { if (Words[ 1 ].Length > 0 ) { fixMode = Words[ 1 ][ 0 ] == ' A ' ? FixMode.Auto : FixMode.Manual; } if (Words[ 2 ].Length > 0 ) { switch (Convert.ToInt32(Words[ 2 ])) { case 1 : fixMethod = FixMethod.NotSet; break ; case 2 : fixMethod = FixMethod.Fix2D; break ; case 3 : fixMethod = FixMethod.Fix3D; break ; default : fixMethod = FixMethod.NotSet; break ; } } foreach (GpsSatellite s in satellites.Values) { s.InUsed = false ; } satellitesInUsed = 0 ; for ( int i = 0 ; i < 12 ; ++ i) { string id = Words[ 3 + i]; if (id.Length > 0 ) { int nId = Convert.ToInt32(id); if ( ! satellites.ContainsKey(nId)) { satellites[nId] = new GpsSatellite(); satellites[nId].PRC = nId; } satellites[nId].InUsed = true ; ++ satellitesInUsed; } } if (Words[ 15 ].Length > 0 ) { positionDilutionOfPrecision = Convert.ToDecimal(Words[ 15 ]); } if (Words[ 16 ].Length > 0 ) { horizontalDilutionOfPrecision = Convert.ToDecimal(Words[ 16 ]); } if (Words[ 17 ].Length > 0 ) { verticalDilutionOfPrecision = Convert.ToDecimal(Words[ 17 ]); } return true ; } 代码4
从ParseGPGSA看,字段1为fix的状态,手工fix或者自动fix。字段2为fix的方法,2D或者3D。字段3到14共12个字段分别为在使用卫星的信息。字段15为位置精度信息。字段16为水平精度信息。字段17为垂直精度信息。
private bool ParseGPGSV( string [] Words) { int messageNumber = 0 ; if (Words[ 2 ].Length > 0 ) { messageNumber = Convert.ToInt32(Words[ 2 ]); } if (Words[ 3 ].Length > 0 ) { satellitesInView = Convert.ToInt32(Words[ 3 ]); } if (messageNumber == 0 || satellitesInView == 0 ) { return false ; } for ( int i = 1 ; i <= 4 ; ++ i) { if ((Words.Length - 1 ) >= (i * 4 + 3 )) { int nId = 0 ; if (Words[i * 4 ].Length > 0 ) { string id = Words[i * 4 ]; nId = Convert.ToInt32(id); if ( ! satellites.ContainsKey(nId)) { satellites[nId] = new GpsSatellite(); satellites[nId].PRC = nId; } satellites[nId].InView = true ; } if (Words[i * 4 + 1 ].Length > 0 ) { satellites[nId].Elevation = Convert.ToInt32(Words[i * 4 + 1 ]); } if (Words[i * 4 + 2 ].Length > 0 ) { satellites[nId].Azimuth = Convert.ToInt32(Words[i * 4 + 2 ]); } if (Words[i * 4 + 3 ].Length > 0 ) { satellites[nId].SNR = Convert.ToInt32(Words[i * 4 + 3 ]); satellites[nId].NotTracking = false ; } else { satellites[nId].NotTracking = true ; } } } return true ; } 代码5
从ParseGPGSA看,这个比较特别,他把在使用的卫星信息分开多条语句output。如下:
$GPGSV, 3 , 1 , 12 , 03 , 43 , 246 , 46 , 06 , 57 , 263 , 52 , 09 , 10 , 090 , 00 , 14 , 29 , 357 , 41 * 71 $GPGSV, 3 , 2 , 12 , 15 , 12 , 140 , 00 , 16 , 10 , 307 , 00 , 18 , 59 , 140 , 00 , 19 , 20 , 224 , 00 * 75 $GPGSV, 3 , 3 , 12 , 21 , 48 , 089 , 00 , 22 , 69 , 265 , 36 , 24 , 09 , 076 , 00 , 34 , 00 , 000 , 00 * 76 字段1为一共分开多少条语句。字段2为当前语句的序号。字段3为在使用的卫星的数量。后面字段分别表示三个不同卫星的信息,取其中一个卫星来解释,字段4为卫星的ID,字段5为太空海拔,字段6为角度,字段7为信号强弱。
对于厂商的私有NMEA data也是同样的方法进行分析,根据文档的描述进行分析。下面为整个类的代码。
NmeaParser public class NmeaParser { public struct Coordinate { public int Hours; public int Minutes; public double Seconds; } public enum FixStatus { NotSet, Obtained, //A Lost //V } public enum FixMode { Auto, //A Manual } public enum FixMethod { NotSet, Fix2D, Fix3D } public enum DifferentialGpsType { NotSet, SPS, DSPS, PPS, RTK } public class GpsSatellite { public int PRC { get; set; } public int Elevation { get; set; } public int Azimuth { get; set; } public int SNR { get; set; } public bool InUsed { get; set; } public bool InView { get; set; } public bool NotTracking { get; set; } } private static readonly CultureInfo NmeaCultureInfo = new CultureInfo("en-US"); private static readonly decimal KMpHPerKnot = decimal.Parse("1.852", NmeaCultureInfo); private Coordinate latitude; private Coordinate longitude; private decimal altitude = 0; private DateTime utcDateTime; private decimal velocity = 0; private decimal azimuth = 0; private FixStatus fixStatus; private DifferentialGpsType differentialGpsType; private FixMode fixMode; private FixMethod fixMethod; private int satellitesInView; private int satellitesInUsed; private readonly Dictionary<int, GpsSatellite> satellites; private decimal horizontalDilutionOfPrecision = 50; private decimal positionDilutionOfPrecision = 50; private decimal verticalDilutionOfPrecision = 50; public NmeaParser() { satellites = new Dictionary<int, GpsSatellite>(); } public bool Parse(string sentence) { string rawData = sentence; try { if (!IsValid(sentence)) { return false; } sentence = sentence.Substring(1, sentence.IndexOf('*') - 1); string[] Words = Getwords(sentence); switch (Words[0]) { case "GPRMC": return ParseGPRMC(Words); case "GPGGA": return ParseGPGGA(Words); case "GPGSA": return ParseGPGSA(Words); case "GPGSV": return ParseGPGSV(Words); default: return false; } } catch (Exception e) { Console.WriteLine(e.Message + rawData); return false; } } private bool IsValid(string sentence) { // GPS data can't be zero length if (sentence.Length == 0) { return false; } // first character must be a $ if (sentence[0] != '$') { return false; } // GPS data can't be longer than 82 character if (sentence.Length > 82) { return false; } try { string checksum = sentence.Substring(sentence.IndexOf('*') + 1); return Checksum(sentence, checksum); } catch (Exception e) { Console.WriteLine("Checksum failure. " + e.Message); return false; } } private bool Checksum(string sentence, string checksumStr) { int checksum = 0; int length = sentence.IndexOf('*') - 1; // go from first character upto last * for (int i = 1; i <= length; ++i) { checksum = checksum ^ Convert.ToByte(sentence[i]); } return (checksum.ToString("X2") == checksumStr); } // Divides a sentence into individual Words private static string[] Getwords(string sentence) { return sentence.Split(','); } private bool ParseGPRMC(string[] Words) { if (Words[1].Length > 0 & Words[9].Length > 0) { int UtcHours = Convert.ToInt32(Words[1].Substring(0, 2)); int UtcMinutes = Convert.ToInt32(Words[1].Substring(2, 2)); int UtcSeconds = Convert.ToInt32(Words[1].Substring(4, 2)); int UtcMilliseconds = 0; // Extract milliseconds if it is available if (Words[1].Length > 7) { UtcMilliseconds = Convert.ToInt32(Words[1].Substring(7)); } int UtcDay = Convert.ToInt32(Words[9].Substring(0, 2)); int UtcMonth = Convert.ToInt32(Words[9].Substring(2, 2)); // available for this century int UtcYear = Convert.ToInt32(Words[9].Substring(4, 2)) + 2000; utcDateTime = new DateTime(UtcYear, UtcMonth, UtcDay, UtcHours, UtcMinutes, UtcSeconds, UtcMilliseconds); } fixStatus = (Words[2][0] == 'A') ? FixStatus.Obtained : FixStatus.Lost; if (Words[3].Length > 0 & Words[4].Length == 1 & Words[5].Length > 0 & Words[6].Length == 1) { latitude.Hours = int.Parse(Words[3].Substring(0, 2)); latitude.Minutes = int.Parse(Words[3].Substring(2, 2)); latitude.Seconds = Math.Round(double.Parse(Words[3].Substring(5, 4)) * 6 / 1000.0, 3); if ("S" == Words[4]) { latitude.Hours = -latitude.Hours; } longitude.Hours = int.Parse(Words[5].Substring(0, 3)); longitude.Minutes = int.Parse(Words[5].Substring(3, 2)); longitude.Seconds = Math.Round(double.Parse(Words[5].Substring(6, 4)) * 6 / 1000.0, 3); if ("W" == Words[6]) { longitude.Hours = -longitude.Hours; } } if (Words[8].Length > 0) { azimuth = decimal.Parse(Words[8], NmeaCultureInfo); } if (Words[7].Length > 0) { velocity = decimal.Parse(Words[7], NmeaCultureInfo) * KMpHPerKnot; } return true; } private bool ParseGPGGA(string[] Words) { if (Words[6].Length > 0) { switch (Convert.ToInt32(Words[6])) { case 0: differentialGpsType = DifferentialGpsType.NotSet; break; case 1: differentialGpsType = DifferentialGpsType.SPS; break; case 2: differentialGpsType = DifferentialGpsType.DSPS; break; case 3: differentialGpsType = DifferentialGpsType.PPS; break; case 4: differentialGpsType = DifferentialGpsType.RTK; break; default: differentialGpsType = DifferentialGpsType.NotSet; break; } } if (Words[7].Length > 0) { satellitesInUsed = Convert.ToInt32(Words[7]); } if (Words[8].Length > 0) { horizontalDilutionOfPrecision = Convert.ToDecimal(Words[8]); } if (Words[9].Length > 0) { altitude = Convert.ToDecimal(Words[9]); } return true; } private bool ParseGPGSA(string[] Words) { if (Words[1].Length > 0) { fixMode = Words[1][0] == 'A' ? FixMode.Auto : FixMode.Manual; } if (Words[2].Length > 0) { switch (Convert.ToInt32(Words[2])) { case 1: fixMethod = FixMethod.NotSet; break; case 2: fixMethod = FixMethod.Fix2D; break; case 3: fixMethod = FixMethod.Fix3D; break; default: fixMethod = FixMethod.NotSet; break; } } foreach (GpsSatellite s in satellites.Values) { s.InUsed = false; } satellitesInUsed = 0; for (int i = 0; i < 12; ++i) { string id = Words[3 + i]; if (id.Length > 0) { int nId = Convert.ToInt32(id); if (!satellites.ContainsKey(nId)) { satellites[nId] = new GpsSatellite(); satellites[nId].PRC = nId; } satellites[nId].InUsed = true; ++satellitesInUsed; } } if (Words[15].Length > 0) { positionDilutionOfPrecision = Convert.ToDecimal(Words[15]); } if (Words[16].Length > 0) { horizontalDilutionOfPrecision = Convert.ToDecimal(Words[16]); } if (Words[17].Length > 0) { verticalDilutionOfPrecision = Convert.ToDecimal(Words[17]); } return true; } private bool ParseGPGSV(string[] Words) { int messageNumber = 0; if (Words[2].Length > 0) { messageNumber = Convert.ToInt32(Words[2]); } if (Words[3].Length > 0) { satellitesInView = Convert.ToInt32(Words[3]); } if (messageNumber == 0 || satellitesInView == 0) { return false; } for (int i = 1; i <= 4; ++i) { if ((Words.Length - 1) >= (i * 4 + 3)) { int nId = 0; if (Words[i * 4].Length > 0) { string id = Words[i * 4]; nId = Convert.ToInt32(id); if (!satellites.ContainsKey(nId)) { satellites[nId] = new GpsSatellite(); satellites[nId].PRC = nId; } satellites[nId].InView = true; } if (Words[i * 4 + 1].Length > 0) { satellites[nId].Elevation = Convert.ToInt32(Words[i * 4 + 1]); } if (Words[i * 4 + 2].Length > 0) { satellites[nId].Azimuth = Convert.ToInt32(Words[i * 4 + 2]); } if (Words[i * 4 + 3].Length > 0) { satellites[nId].SNR = Convert.ToInt32(Words[i * 4 + 3]); satellites[nId].NotTracking = false; } else { satellites[nId].NotTracking = true; } } } return true; } }
参考文献:
转载于:https://www.cnblogs.com/procoder/archive/2009/05/06/Windows-Mobile-GPS-NMEA.html