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

  • 吳明恩

    您好 感謝您的教學 我想請問一下 我下載您的程式碼後 轉APK在 手機上執行

    但經緯度 衛星品質 跟 衛星數量都讀取不到!? 也有將 手機 放至 陽台測試 也是無法讀取 請問有 什麼 方法 可以解決嗎!? 感謝您

    • 有無定位到資訊基本上NMEA資訊都會出來,您可以試著註冊”NmeaListener”將NMEA的RAW DATA印出來看看資訊是否有出來:

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

      這必需要你同意存取GPS的權限,如果資料都沒出來就有可能您的手機不支援,提供您參考

      • 吳明恩

        感謝您的回答 結果我發現 是我的 ADNROID版本選錯 我選到6.0版本 結果 NMEA沒辦法動作 改成4.4 就可以正常動作了

        • 範例還未在5.0以上測試過這點忘了提醒

  • 吳明恩

    我想在請在問一下 要如何 才可以把 顯示的資訊 停留久一點 現在顯示的資訊 一下 就跳掉了 有辦法嗎!?

    • 不太了解您的意思,能說明清楚一點嗎?

      • 吳明恩

        就是 我想多加 NMEA的GPGSV近來 顯示他的相關資訊 但是我 不知道要如何加入 我寫進去都無法顯示 還有 就是 顯示 經緯度那些資料的時候 可以 把 顯示的資料停留久一點 不會 立刻消失 有辦法嗎!?

        • 你可以將程式改成收到GPGSV與GGPSA時都將訊息利用handler送出 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("$GPGSV")){
                  //send GGA nmea data to handler
                  Message msg = new Message();
                  msg.obj = rawNmea;
                  mHandler.sendMessage(msg);
              }
          
          }`
          
          • 吳明恩

            這樣程式會 直接跳掉@” @

          • 提供一下錯誤訊息喔

          • 吳明恩

            就是 程式一打開 就閃退了

  • 吳明恩

    我想請問一下您一下 如果 想計算 經緯度定位完成的時間 並顯示出來 要怎麼計算阿!? 我看 NMEA的規格內 都只有 顯示 現在的時間 並沒有 定位完成的 請問您知道嗎!?

    • 你可以試著抓GPGGA的第6位GPS Quality的值來確定定位成功的時間。

      • 吳明恩

        好的 我用Quality進行判斷 看看 對嘞我 想請問您一下 我 放GPGSA的衛星編號 進程式內 定位完成後 結果他只會顯示 一顆衛星編號而已 不會 顯示出 其他的衛星編號

        請問 您知道 要怎樣解決嗎!?

        • 你要抓一下GPGSV,它會顯示已定位衛星資訊,一次最多顯示四組衛星,超過的下一次GPGSV會顯示,你要把所有資訊都整合好後就可以知道有定到幾顆並且知道它的詳細資訊。

          eg.

          $GPGSV,3,1,11,03,03,111,00,04,15,270,00,06,01,010,00,13,06,292,0074 $GPGSV,3,2,11,14,25,170,00,16,57,208,39,18,67,296,40,19,40,246,0074 $GPGSV,3,3,11,22,42,067,42,24,14,311,43,27,05,244,00,,,,*4D

          1) 3 = 天空中收到訊號的衛星總數。 2) 1 = 定位的衛星總數。 3) 09 = 天空中的衛星總數,00 至 12。 4) 01 = 衛星編號, 01 至 32。 5) 27 = 衛星仰角, 00至 90 度。 6) 299 = 衛星方位角, 0 至 359 度。實際值。 7) 43 = 訊號雜訊比(C/No), 00 至 99 dB;無表未接收到訊號

          4~7會重覆,最多四組,超過的下一會放在下一次GPGSV

          格式:$GPGSV,1,2,3,[4,5,6,7][8,9,10,11][12,13,14,15][16,17,18,19]

          • 吳明恩

            不太懂@”@ 我是 這樣寫 txtGPS_Prn.setText(rawNmeaGSV[4]+”,”); 但是 要怎樣 把全部編號顯示出來??

          • 你的寫法只會顯示一組衛星資訊,每個GSV最多顯示四組衛星資訊,如我範例中的[],[],[],[],如果你定位的衛星數超過四組,那就會再下一列的GSV顯示。

          • 吳明恩

            所以是

            txtGPS_Prn.setText(rawNmeaGSV[4]+”,”+rawNmeaGSV[5]+”,”); 類似ˊ這樣?

          • txtGPS_Prn.setText(rawNmeaGSV[4]+”,”+rawNmeaGSV[8]+”,”…….);

          • 吳明恩

            想請問 您一下 前四顆衛星編號 是這樣 [4] [8] [12] [16] 第5-9科 的衛星編號是 [20] [24] [28] [32] 還是 一樣抓 [4] [8] [12] [16] !?

          • $GPGSV,1,2,3,[4,5,6,7][8,9,10,11][12,13,14,15][16,17,18,19]

            ——————No1—,—No2—-,No3,———,No4——— 是這樣子的

          • 吳明恩

            不太懂您意思

            假設 我要讀第5-9科衛星編號 如何才能抓取第二行的GPGSV

            我 目前 是 用 讀取 20 24 28 32 跟 再抓一次4 8 12 16位置但是好像都是錯的 請問 如何更改呢!? 感謝您^O^

          • 上面例子為提供4,8,12,16衛星資訊,每次GSV出來會把定位好的衛星顯示出來(不一定照順序),你不斷的收NMEA資料時就會收到很多的GSV資料,你再自行將其中的衛星資訊定位的資料都抓出來並且顯示出來

          • 吳明恩

            所以 如果 是 抓超過5科 衛星以上 要將 nmea存到 不同的 地方 分別 抓取 並 顯示!?

          • 對啊,你要另外自已存起來後整理完再顯示

  • 吳明恩

    我衛星編號 抓取 成功了 現在定位時間 有點怪怪的 再一次定位時 時間會是 正確的 但是 他繼續抓資料後 他定位的時間 就從 剛剛定位到的時間 繼續跑 有辦法 後面定位的時間 歸零 後再定位媽!? 我抓取時間的 方法是 protected void onCreate(Bundle savedInstanceState) {

    super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); startTime = System.currentTimeMillis();

    public void handleMessage(Message msg) {

    Long spentTime = System.currentTimeMillis() – startTime; Long seconds = (spentTime/1000) % 60; 但是 他 第二次定位時間 會 有問題 請問 您 有辦法嗎!?

    • 定位好後你可能要重新再抓一次時間存到starTime

      • 吳明恩

        您的意思是

        public void handleMessage(Message msg) {

        Long spentTime = System.currentTimeMillis() – startTime; Long seconds = (spentTime/1000) % 60;

        startTime = System.currentTimeMillis(); 這樣嗎!? 可是 我這樣 轉成APK在 手機測驗 程式 會直接跳出去 是 startTime = System.currentTimeMillis(); 這不能放在 HANDLE裡面媽!?

        • 如果你的handleMessage有改UI就要在 UI Thread!,建 議你開啟手機的除錯模式配合Android Studio 的debug log看一下問題在哪