读取Android内建GPS的NMEA资讯

读取Android内建GPS的NMEA资讯

如何在Android取得GPS定位资料在很多文章或是教学中都很容易了解及使用,但要如何像其他APP一样可以取得更详细的内容呢?这就要利用Android在GpsStatus物件中的Listener:GpsStatus.NmeaListener,当GPS启动时就会立即的收到GPS统一规格NMEA0831格式资料,解析这些资料就能得到:座标、卫星有效数量、速度…等一些GPS的详细资讯,所以自已要写一个了解GPS状态并不是难事!

取得LocationManager服务

GPS或其他相关的定位服务由LocationManager来管理,我们必需要先取得LocationManager的物件及注册Listener才能使用最基础的定位服务,下面先取得定位服务后,再注册名为locationListener的Listener。

//-----------start-----------
    LocationManager locationManager=(LocationManager)getSystemService(Context.LOCATION_SERVICE);
    locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 5000, 0, locationListener);

//------------end------------

范例中直接指定定位更新的来源提供者为GPS,不加入其他定位来源的判断,其中上述的locationListener的Listener内容:

//-----------start-----------
        LocationListener locationListener=new LocationListener(){

            @Override
            public void onLocationChanged(Location loc) {
                // TODO Auto-generated method stub
                //定位资料更新时会回呼

            }

            @Override
            public void onProviderDisabled(String provider) {
                // TODO Auto-generated method stub
                //定位提供者如果关闭时会回呼,并将关闭的提供者传至provider字串中
            }

            @Override
            public void onProviderEnabled(String provider) {
                // TODO Auto-generated method stub
                //定位提供者如果开启时会回呼,并将开启的提供者传至provider字串中
            }

            @Override
            public void onStatusChanged(String provider, int status, Bundle extras) {
                // TODO Auto-generated method stub
                Log.d("GPS-NMEA", provider + "");
                //GPS状态提供,这只有提供者为gps时才会动作
                switch (status) {
                case LocationProvider.OUT_OF_SERVICE:
                    Log.d("GPS-NMEA","OUT_OF_SERVICE");
                    break;
                case LocationProvider.TEMPORARILY_UNAVAILABLE:
                    Log.d("GPS-NMEA"," TEMPORARILY_UNAVAILABLE");
                    break;
                case LocationProvider.AVAILABLE:
                    Log.d("GPS-NMEA","" + provider + "");

                    break;
                }

            }

        };
//------------end------------

以上程式中提供最基本的方法,如果要取得座标更新讯息其方法为onLocationChanged,例如,你可以将座标印在Log上:

//-----------start-----------
            public void onLocationChanged(Location loc) {
                // TODO Auto-generated method stub
                Log.d("GPS-NMEA", loc.getLatitude() + "," +  loc.getLongitude());
            }
//------------end------------

不过这些基本操作会在其他文章中再说明,这里最大的重点是要注册GpsStatus.NmeaListener的Listener,前面只是做最基本的操作,接下来才能发挥作用。

注册GpsStatus.NmeaListener

先前的注册定位相关资讯后,再来就是要注册GpsStatus.NmeaListener,所以先前的内容会再修改为:

//-----------start-----------
        locationManager=(LocationManager)getSystemService(Context.LOCATION_SERVICE);
        locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, 5000, 0, locationListener);
        locationManager.addNmeaListener(nmeaListener);
//------------end------------

其中上述的nmeaListener的Listener内容:

        GpsStatus.NmeaListener nmeaListener = new GpsStatus.NmeaListener() {
            public void onNmeaReceived(long timestamp, String nmea) {
                //check nmea's checksum
                Log.d("GPS-NMEA", nmea);

            }
    };

在方法onNmeaReceived中,只要系统每次取得GPS资料时就会呼叫此方法,并将资料传前字串nmea,此时再处理这个字串内容就行,在这我们将每次收到的讯息印出来,印出来的资讯类似下面例子:

$PGLOR,0,NEW,PERFIX,1,PER,1,QOP,50*36
$PGLOR,0,RID,BCD,2,18,2,96441*77
$GPGGA,072515.33,,,,,0,00,1000.0,,M,,M,,*53
$PGLOR,0,STA,072515.33,0.000,0.707,175,98,10000,0,P,D,L,1,C,0,S,0004,0*54
$PGLOR,1,SAT,G03,035,13,G16,036,13,G20,023,11,G27,034,13,G32,031,13*6F
$PGLOR,0,SIO,TxERR,0,RxERR,0,TxCNT,3444,RxCNT,3360,DTMS,2059,DTIN,6,DTOUT,201,HATMD,0*0A
$PGLOR,0,HLA,072515.33,L,3,Al,,A,,H,,,M,0,Ac,0,Gr,0,S,,,Sx,,,T,0,Tr,,Mn,0*01
$GPGSV,2,1,05,03,01,000,35,16,01,000,36,20,01,000,23,27,01,000,34*79
$GPGSV,2,2,05,32,01,000,31*4E
$GPGSA,A,1,,,,,,,,,,,,,140.0,99.0,99.0*35
$GNGSA,A,1,,,,,,,,,,,,,140.0,99.0,99.0*2B
$QZGSA,A,1,,,,,,,,,,,,,140.0,99.0,99.0*29
$GPRMC,072515.33,V,,,,,,,090414,,,N*71
$GPGGA,072516.33,,,,,0,00,1000.0,,M,,M,,*50
$PGLOR,0,STA,072516.33,0.000,0.707,175,98,10000,0,P,D,L,1,C,1,S,0000,0*52
$PGLOR,1,SAT,G16,037,13,G32,032,13,G27,034,13,G03,036,13*36
$PGLOR,0,SIO,TxERR,0,RxERR,0,TxCNT,1792,RxCNT,2996,DTMS,970,DTIN,7,DTOUT,152,HATMD,-27*2E
$PGLOR,0,HLA,072516.33,L,,Al,,A,,H,,,M,0,Ac,0,Gr,0,S,,,Sx,,,T,0,Tr,,Mn,0*31
$GPGSV,3,1,10,16,46,251,37,32,34,249,32,27,19,189,34,03,07,203,36*7F
$GPGSV,3,2,10,31,51,019,,14,39,138,,29,25,053,,20,17,286,*70
$GPGSV,3,3,10,23,10,320,,25,03,046,*7F
$GPGSA,A,1,,,,,,,,,,,,,140.0,99.0,99.0*35
$GNGSA,A,1,,,,,,,,,,,,,140.0,99.0,99.0*2B
$QZGSA,A,1,,,,,,,,,,,,,140.0,99.0,99.0*29


此时,你就完成读取GPS NMEA资料的工作,但你取得此资料后还是必需要解析NMEA资料才能正确的取得资讯,后面会利用一个范例来取得GPS定位状态、卫星有效数量、座标…等资讯。

执行范例:

执行范例之前必需要提醒一点,你要自行先将GPS开启,范例不会对GPS是否开启做侦测的动作,当然你也可以试着自行加入该程式内容让范例程式更加符合实际需要。

检查NMEA误码资料(checksum)

NMEA规格中规定必需要对NMEA检查checksum是否正确,这样才能确保资料内容正确后再去解析资料,这里我将检查方式写程一个方法,需要的可以从范例撷取使用。

NMEA规格中将每串资料最后一笔从的*号后到结尾内容为checksum的值,例如:

$GPGSA,A,1,,,,,,,,,,,,,140.0,99.0,99.0*35

其取得的checksum为35,而35的值为十六进制的35,再利用checksum特性将格式中的$*之间内容加总计算,与取得的比对,正确时,此资料为有效资料。

$......加总........*

所以我所写的checksum程式如下,资料为有效值会返回true

//-----------start-----------
    private boolean isValidForNmea(String rawNmea){
        boolean valid = true;
        byte[] bytes = rawNmea.getBytes();
        int checksumIndex = rawNmea.indexOf("*");
        //NMEA 星号后为checksum number
        byte checksumCalcValue = 0;
        int checksumValue;

        //检查开头是否为$
        if ((rawNmea.charAt(0) != '$') || (checksumIndex==-1)){
            valid = false;
        }
        //
        if (valid){
            String val = rawNmea.substring(checksumIndex + 1, rawNmea.length()).trim();
            checksumValue = Integer.parseInt(val, 16);
            for (int i = 1; i < checksumIndex; i++){
                checksumCalcValue = (byte) (checksumCalcValue ^ bytes[i]);
            }
            if (checksumValue != checksumCalcValue){
                valid = false;
            }
        }
        return valid;
    }
//------------end------------

移除Listener

不使用时记得将Listener给移除:

//-----------start-----------
    locationManager.removeUpdates(locationListener);
    locationManager.removeNmeaListener(nmeaListener);
//------------end------------

正确来说是必需要自行移除,如果你忘记移除时,系统虽然还防止问题产生,在过程中还是会正常运作,但不增加系统上的负担,Listener不使用时还是移除比较好。

处理NMEA资料

在每次取得NMEA资料会进行检查,再将NMEA资料传递至副程式nmeaProgress

//-----------start-----------
        nmeaListener = new GpsStatus.NmeaListener() {
            public void onNmeaReceived(long timestamp, String nmea) {
                //check nmea's checksum
                if (isValidForNmea(nmea)){
                    nmeaProgress(nmea);
                    Log.d("GPS-NMEA", nmea);
                }

            }
    };
//------------end------------

因为读取NMEA的Listener并非在UI执行绪之中,利用nmeaProgress将NMEA资料传至自建的Handler,确保在UI执行绪中能将资料传至UI控制元件:

    private void nmeaProgress(String rawNmea){

        String[] rawNmeaSplit = rawNmea.split(",");

        if (rawNmeaSplit[0].equalsIgnoreCase("$GPGGA")){
            //send GGA nmea data to handler
            Message msg = new Message();
            msg.obj = rawNmea;
            mHandler.sendMessage(msg);
        }

    }

程式中必需要注意一段:

        if (rawNmeaSplit[0].equalsIgnoreCase("$GPGGA")){
:
:
:
        }

$GPGGA为NMEA0831所定义的,下图为NMEA0831描述的规格,我也有将格式贴于范例中,

中文说明:

<1>  UTC时间,格式为hhmmss.sss。
<2>  纬度,格式为ddmm.mmmm(前导位数不足则补0)。
<3>  纬度半球,N或S(北纬或南纬)。
<4>  经度,格式为dddmm.mmmm(前导位数不足则补0)。
<5>  经度半球,E或W(东经或西经)。
<6>  定位品质指示,0=定位无效,1=定位有效,2=差分定位(有效)。
<7>  使用卫星数量,从00到12(前导位数不足则补0)。
<8>  水平精确度,0.5到99.9。
<9>  天线离海平面的高度,-9999.9到9999.9米
<10> 高度单位,M表示单位米。
<11> 大地椭球面相对海平面的高度(-999.9到9999.9)。
<12> 高度单位,M表示单位米。
<13> 差分GPS数据期限(RTCM SC-104),最后设立RTCM传送的秒数量。
<14> 差分参考基站标号,从0000到1023(前导位数不足则补0)。
<15> checksum校验和。

每项资料都使用,分隔,每项资料都有其意义,范例中取用GGA中的2、3、4、5、6、7的资料, 并将处理过程写在Handler内容中。

Handler内容如下:

//-----------start-----------
    mHandler = new Handler() {
        public void handleMessage(Message msg) {

            String str = (String) msg.obj;
            String[] rawNmeaSplit = str.split(",");
            txtGPS_Quality.setText(rawNmeaSplit[6]);
            txtGPS_Location.setText(rawNmeaSplit[2] + " " + rawNmeaSplit[3] + "," + rawNmeaSplit[4] + " " + rawNmeaSplit[5]);
            txtGPS_Satellites.setText(rawNmeaSplit[7]);

        }
        };

}
//------------end------------

依照格式中的第6笔为GPS定位品质,第2~5为定位座标,如果未定位成功在这里得到的资料会是空白,最后第7笔为已经接收到的卫星有效数量,再将取得的资料传至UI控制元件的TextView,所以看到的结果如下:

以上为最基本的读取内建GPS的NMEA资料方式,如果将NMEA的资料加强利用时,提供的GPS状态将更详细,利用NMEA读取的技巧与解析方式也能用在蓝牙的外部GPS接收器,将从蓝牙取得的NMEA资料加以利用。

范例程式

参考资料

NMEA 0831