讀取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