Android解析GPS卫星的NMEA数据
关于NMEA
GPS模块会上报NMEA协议的字符串,NMEA也分很多类数据,里面包含时间、卫星信息、经纬度等。
Android framework如何使用NMEA
可以参考我的上一篇文章: “android GPS框架”
总的来说,正常情况下,framework层不参与NMEA数据解析,解析的工作由HAL层完成
但如果你想要外挂GPS模块,或者伪造一些NMEA数据,都可以让在framework自己进行解析,供原生Location相关API使用。具体要在GnssLocationProvider类中拿到NMEA或NMEA解析后的数据,然后调用该类的reportSvStatus注入卫星信息,调用reportLocation注入定位信息。
使用哪类NMEA数据
我自己使用ZDA数据获取时间;使用GSV数据获取卫星信息;使用GGA数据获取定位经纬度信息
解析NMEA数据的坑
1,导航信息回调流程中(GnssLocationProvider.reportLocation)封装成Location类,成员变量mProvider对应了定位监听注册函数LocationManager.requestLocationUpdates中第一个入参String provider,一般都是“gps”,所以构建用于定位的Location类时mProvider也要赋值“gps”
2,GPS状态回调流程中(GnssLocationProvider.reportSvStatus)需要解析得到mSvidWithFlags,这个值在后面会有大量的位移和标志位校验操作,必须设置正确。详见GpsStatus.setStatus中的代码。
我们从NMEA中拿到的svid一般是从1开始的数值,首先我们要将svid左移:
svid << GnssStatus.SVID_SHIFT_WIDTH
然后我们根据GSV所属的发送器标识符来确定卫星类型,如BDGSV就是北斗,我们要根据卫星类型,加上掩码:
//判断卫星类型
if(sField[0].equalsIgnoreCase("$GLGSV")) {
mSvidWithFlags[i] += GnssStatus.CONSTELLATION_GLONASS << GnssStatus.CONSTELLATION_TYPE_SHIFT_WIDTH;
}else if(sField[0].equalsIgnoreCase("$BDGSV")) {
mSvidWithFlags[i] += GnssStatus.CONSTELLATION_BEIDOU << GnssStatus.CONSTELLATION_TYPE_SHIFT_WIDTH;
}else if(sField[0].equalsIgnoreCase("$GPGSV")) {
mSvidWithFlags[i] += GnssStatus.CONSTELLATION_GPS << GnssStatus.CONSTELLATION_TYPE_SHIFT_WIDTH;
}else {
mSvidWithFlags[i] += GnssStatus.CONSTELLATION_QZSS << GnssStatus.CONSTELLATION_TYPE_SHIFT_WIDTH;
}
然后再根据实际情况,加上是否携带某类数据的标识:
mSvidWithFlags[i] += GnssStatus.GNSS_SV_FLAGS_HAS_EPHEMERIS_DATA + GnssStatus.GNSS_SV_FLAGS_HAS_ALMANAC_DATA + GnssStatus.GNSS_SV_FLAGS_USED_IN_FIX;
3,NMEA数据是带校验位的,如“$GPGGA,235316.000,2959.9925,S,12000.0090,E,1,06,1.21,62.77,M,0.00,M,,*7B” 其中“*”后面的7B就是$和"*"之间所有字符的异或结果,因为有时候以",12*5A"结尾,有时候又是",*7B",要小心split后最后一位是空数据拿不到,导致数组下标少一位,取数据时容易出错,我们预处理时可以这么搞:
data.replaceAll(",\\*\\w\\w",",0").replaceAll("\\*\\w\\w","").replace("\n","").replace("\r","").split(",");
这样如果是",*7B"会被转成“0”,而",12*5A"转换为“12”
4,要注意经纬度换算!
GGA中的经纬度是ddmm.mmmm和dddmm.mmmm(d代表度,m代表分),实际传给高德等导航软件,需要将分转化为度。比如1234.5678,转换算法为:12+34.5678/60
5,要注意时区
ZDA里的时间字符转成Date是UTC时间,而系统校时所需的Date是要加上时区的。我的做法如下:
//处理ZDA数据
if (sField[0].contains("ZDA") && sField.length >= 5) {
//拼接时间格式"yyyy-MM-dd HH:mm:ss"
StringBuffer sb = new StringBuffer();
sb.append(sField[4]).append("-").append(sField[3]).append("-").append(sField[2]).append(" ").append(sField[1].substring(0,2)).
append(":").append(sField[1].substring(2,4)).append(":").append(sField[1].substring(4,6));
Log.d(TAG, "ZDA : HIK receive time = " + sb.toString());
SimpleDateFormat sdfUTC = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
sdfUTC.setTimeZone(TimeZone.getTimeZone("UTC")); // 设置UTC时区。如果不设置,默认为当前时区时间
try {
mDate = sdfUTC.parse(sb.toString());//生成与时区无关的Date。但SimpleDateFormat 必须设置正确时区,因为生成Date时如果是别的时区,会转成UTC时间
Log.d(TAG, "ZDA : HIK receive long time = " + mDate.getTime());
} catch (ParseException e) {
e.printStackTrace();
}
}