如何寫GPS定位的程式碼 C# VS2005 Sample Code

延續前一篇"讓人知道你在那",本篇將DEMO如何寫GPS定位的程式碼 C# VS2005 Sample Code

之前有寫了一篇"讓人知道你在那"的文章,希望能自己寫程式去運用GPS的定位功能,而這篇文章將會帶到兩個部份,第一個部份是PDA回傳資料時所用的 Web Service,另一個就是PDA的GPS訊號抓取與轉換,而這部份並不是採用PAPAGO或其它的SDK,全部是採自己Coding的做法,在寫這個過程中,其實有點頭大,因為Coding的電腦是在室內,所以PDA是無法抓到GPS的訊號,所以只好從戶外拉了一條好長的GPS延長線到電腦邊,最後從抓到的經緯度丟到google map上去顯示,定出來的位置還算準確,確實定位出目前我的所在位置,另一個常在一般的衛星導航系統看到的情況,就是有時會亂跳,因為車子不可能在可行駛的道路以外,所以當經緯度偏離航道時,就會開始"亂跳",想辦法跳回最近的航道,這有好有壞,有時GPS是對的,位置真的不在道路上,但導航系統一直挑附近的道路跳,可是有時GPS訊號差,經緯度的誤差很大,明明在這條巷子,結果地圖顯示在另一條巷子裡,而導航程式本來就是用來帶領駕駛人行駛方向的,所以"車子"當然要在道路上.

 

廢話了一堆,接下來就開始進入這次的主題,首先,為了儲存PDA所回傳的資料,所以在DataBase開了一個Table來儲存,資料結構大至上如下 :

資料庫 : GPSDB

TableName : GPSTrace

Columns :

UID (int,非null) 識別自動加1

Latitude (nvarchar(10),非null) 預設值 25.048346

Longitude(nvarchar(10),非null) 預設值 121.516396

UpdTime (DateTime,非null) 預設值 getdate()

資料結構大致如上,另外安全性的帳號問題,就各位依自己的喜好去設,配合GPS及下篇MAP展示做修改即可.

 

此次的DEMO方向是PDA會回來Update資料,並不會做Insert的動作,所以只會儲存最新的所在位置,而第一次DB並沒有資料,所以 Update一定會出錯,由於資料操作並非此次重點,因此先手動輸入兩筆資料進去.

SampleData

Web Service的部份也很簡單,只是很單純的Update的動作. 所以以下的Code只是為了達到Update的DEMO目地,其它安全性,例如Connection string不應直接寫在這裡面,這部份就依個人環境需求去改,如果再帶下去,文章會又臭又長,如果有需要,再額外開篇來討論.

 

DB及Web Service好了,接下來要帶的就是PDA的GPS程式寫法(前面帶很快,因為我比較有興趣的是GPS的抓法^^),先大致看一下PDA的UI設計. 最上方的GPS Port是供使用者選擇GPS所使用的Port,因為每台Smart Device的GPS Port不見得相同,其它就都是唯讀的資訊,而資訊來源就是衛星訊號,從UI上看到的資訊只是衛星訊號的一部份,而衛星的訊號也只是抓其中的一種 GPGGA出來用,其它還有GPGSV,GPGSA等等可以運用,衛星的訊號請參考GPS - NMEA sentence Information所提供的資料,在此就不再累述這些訊號一次.

 PDA

而這次所運用的GPGGA訊號,基本上會抓到這類字串$GPGGA,095031.254,2501.9891,N,12133.8101,E,1,07,7.0,123.9,M,15.0,M,0.0,0000*74

每個逗號隔開視為一個欄位,而每個欄位值所代表的意義如下清單

 

 

Name

Example Data

Description

Sentence Identifier$GPGGAGlobal Positioning System Fix Data
UTC Time095031.25409:50:31
Latitude2501.9891,N北緯
Longitude12133.8101,E東經

Fix Quality:
- 0 = Invalid
- 1 = GPS fix
- 2 = DGPS fix

1Data is from a GPS fix
Number of Satellites077 Satellites are in view
Horizontal Dilution of Precision (HDOP)7.0Relative accuracy of horizontal position
Altitude123.9,M123.9 meters above mean sea level
Height of geoid above WGS84 ellipsoid15.0,M15.0,M
Time since last DGPS update0.0因為採GPS修正,不是DGPS,所以沒有資料
DGPS reference station id0000 
Checksum*74Used by program to check for transmission errors

 

瞭解了從衛星收到這字串的意義後,接下來就是怎麼去轉換經緯度的部份了,經度跟緯度的轉換方式都一樣,所以就拿其中一個來說明.

1. 2501.9891 /100 = 25.019891

2. 把小數點後的值從0.019891直接轉為19891

3. 再取小數點部份的值(19891/ 60)*10000=3315166

4. 由於第二步移了一個0,所以3315166轉小數時,要加一個0進去,所以=0.03315166

5. 所以緯度= 25+0.03315166=25.03315166

同樣的方式,算出經度=121.5635

*註此定出來的點是台北101

 

說完了GPS字串及轉換的方式,接下來就是Coding的部份.

1. 在下拉的ComboBox,供使用者選擇GPS Port的部份,先加入四個Port進去

this.cmbPort.Items.Add("COM1");
this.cmbPort.Items.Add("COM2");
this.cmbPort.Items.Add("COM3");
this.cmbPort.Items.Add("COM4");

 

2.加入一個Timer,Interval=1000

 

3.加入一個Serial Port

 

4. 在[開始]的Button加入一個click event. 如此,將GPS的Port打開,藉由Timer的Tick事件來更新資料

 

  1. private void btnStart_Click(object sender, EventArgs e)   
  2. {   
  3.     serialPort1.PortName = cmbPort.SelectedItem.ToString();//依下拉選擇的com port設定Serial Port    
  4.     try  
  5.     {   
  6.         serialPort1.Open();//開啟serial port    
  7.         txtPortStatus.Text = "";   
  8.     }   
  9.     catch (Exception ex)   
  10.     {   
  11.         txtData.Text = ex.Message;   
  12.     }   
  13.   
  14.     if (serialPort1.IsOpen)//以下為依port的開關狀態來決定UI的動作    
  15.     {   
  16.         txtPortStatus.Text = "開啟";   
  17.         txtQuality.Text = "無";   
  18.         txtSTQty.Text = "0";   
  19.         txtSeaLevel.Text = "0";   
  20.         cmbPort.Enabled = false;   
  21.         btnStart.Enabled = false;   
  22.         btnStop.Enabled = true;   
  23.         timer1.Enabled = true;   
  24.     }   
  25.     else  
  26.     {   
  27.         timer1.Enabled = false;   
  28.         txtPortStatus.Text = "關閉";   
  29.         txtQuality.Text = "無";   
  30.         txtSTQty.Text = "0";   
  31.         txtSeaLevel.Text = "0";   
  32.         cmbPort.Enabled = true;   
  33.         btnStart.Enabled = true;   
  34.         btnStop.Enabled = false;   
  35.     }   
  36. }  

 5. 在[結束]的button加入一個click event. 如此,將gps的port關閉

  1. private void btnStop_Click(object sender, EventArgs e)   
  2. {   
  3.     try  
  4.     {   
  5.         serialPort1.Close();//關閉serial port   
  6.         txtPortStatus.Text = "";   
  7.     }   
  8.     catch (Exception ex)   
  9.     {   
  10.         txtPortStatus.Text = ex.Message;   
  11.     }   
  12.   
  13.     if (serialPort1.IsOpen)//以下為依port的開關狀態來決定UI的動作   
  14.     {   
  15.         txtPortStatus.Text = "開啟";   
  16.         txtQuality.Text = "無";   
  17.         txtSTQty.Text = "0";   
  18.         txtSeaLevel.Text = "0";   
  19.         cmbPort.Enabled = false;   
  20.         btnStart.Enabled = false;   
  21.         btnStop.Enabled = true;   
  22.         timer1.Enabled = true;   
  23.     }   
  24.     else  
  25.     {   
  26.         timer1.Enabled = false;   
  27.         txtPortStatus.Text = "關閉";   
  28.         txtQuality.Text = "無";   
  29.         txtSTQty.Text = "0";   
  30.         txtSeaLevel.Text = "0";   
  31.         cmbPort.Enabled = true;   
  32.         btnStart.Enabled = true;   
  33.         btnStop.Enabled = false;   
  34.         btn_upload.Enabled = false;   
  35.     }   
  36. }  

 6. 在[回報]的Button加入一個click event. 如此回報目前所在位置

  1. private void btn_upload_Click(object sender, EventArgs e)   
  2. {   
  3.     GPSWS.Service1 ws = new GPS.GPSWS.Service1();//將之前做好的WS加入參考使用   
  4.     ws.Url="http://localhost/gpsws/service1.asmx";// 設定Web Service的位址   
  5.     try  
  6.     {   
  7.         ws.UpdTrace(1, txtLatitude.Text, txtLongitude.Text);//上傳至資料庫   
  8.     }   
  9.     catch (Exception ex)   
  10.     {   
  11.         MessageBox.Show(ex.Message);   
  12.     }   
  13. }  

7. 在Timer的Tick事件加入判斷從Serial port的GPS訊息,並呈現於UI上

  1. private void timer1_Tick(object sender, EventArgs e)   
  2. {   
  3.     if (serialPort1.IsOpen)   
  4.     {   
  5.         string GPSData = serialPort1.ReadExisting();//將serialPort所取得的資料存到字串內    
  6.         txtData.Text = GPSData;//就是顯示在UI下方的那個大TextBox    
  7.         string[] gpsArr = GPSData.Split('$');//依$拆成多個字串陣列    
  8.         for (int i = 0; i < gpsArr.Length; i++)   
  9.         {   
  10.             string strTemp = gpsArr[i];   
  11.             string[] lineArr = strTemp.Split(',');   
  12.             if (lineArr[0] == "GPGGA")//因為我們只用到GPGGA的訊息,所以其它的訊息不用    
  13.             {   
  14.                 try  
  15.                 {   
  16.                     //Latitude   
  17.                     Double dLat = Convert.ToDouble(lineArr[2]);   
  18.                     dLat = dLat / 100;   
  19.                     string[] lat = dLat.ToString().Split('.');   
  20.                     string la =(((Convert.ToDouble(lat[1]) / 60)*10000)).ToString("#");   
  21.                     for (int a = 0; a < lat[1].Length; a++)   
  22.                     {   
  23.                         if (lat[1].Substring(a, 1) == "0")   
  24.                         {   
  25.                             la = "0" + la;   
  26.                         }   
  27.                         else  
  28.                         {   
  29.                             break;   
  30.                         }   
  31.                     }   
  32.                     Latitude = lat[0].ToString() + "." + la.Substring(0,6);   
  33.   
  34.                     //Longitude   
  35.                     Double dLon = Convert.ToDouble(lineArr[4]);   
  36.                     dLon = dLon / 100;   
  37.                     string[] lon = dLon.ToString().Split('.');   
  38.                     string lo = (((Convert.ToDouble(lon[1]) / 60)*10000)).ToString("#");   
  39.                     for (int b = 0; b < lon[1].Length; b++)   
  40.                     {   
  41.                         if (lon[1].Substring(b, 1) == "0")   
  42.                         {   
  43.                             lo = "0" + lo;   
  44.                         }   
  45.                         else  
  46.                         {   
  47.                             break;   
  48.                         }   
  49.                     }   
  50.                     Longitude = lon[0].ToString() + "." + lo.Substring(0,6);   
  51.   
  52.                     //Display   
  53.                     txtLatitude.Text = Latitude;   
  54.                     txtLongitude.Text = Longitude;   
  55.                     txtSTQty.Text = lineArr[7];//衛星數   
  56.                     txtSeaLevel.Text = lineArr[9];//海平面高度    
  57.                     switch (lineArr[6])//訊號品質    
  58.                     {   
  59.                         case "0":   
  60.                             txtQuality.Text = "品質太差";   
  61.                             break;   
  62.                         case "1":   
  63.                             txtQuality.Text = "GPS fix(SPS)";   
  64.                             break;   
  65.                         case "2":   
  66.                             txtQuality.Text = "DGPS fix";   
  67.                             break;   
  68.                         case "3":   
  69.                             txtQuality.Text = "PPS fix";   
  70.                             break;   
  71.                         case "4":   
  72.                             txtQuality.Text = "即時性動態測量";   
  73.                             break;   
  74.                         case "5":   
  75.                             txtQuality.Text = "Float RTK";   
  76.                             break;   
  77.                         case "6":   
  78.                             txtQuality.Text = "Estimated";   
  79.                             break;   
  80.                         case "7":   
  81.                             txtQuality.Text = "手動輸入模式";   
  82.                             break;   
  83.                         case "8":   
  84.                             txtQuality.Text = "Simulation mode";   
  85.                             break;   
  86.                         default:   
  87.                             txtQuality.Text = "無";   
  88.                             break;   
  89.                     }   
  90.                     btn_upload.Enabled = true;   
  91.                 }   
  92.                 catch  
  93.                 {   
  94.                     txtLatitude.Text = "GPS 訊號不足";   
  95.                     txtLongitude.Text = "GPS 訊號不足";   
  96.                     txtQuality.Text = "無";   
  97.                     txtSTQty.Text = "0";   
  98.                     txtSeaLevel.Text = "0";   
  99.                     btn_upload.Enabled = false;   
  100.                 }   
  101.             }   
  102.         }   
  103.     }   
  104.     else  
  105.     {   
  106.         txtLatitude.Text = "COM Port 已關閉";   
  107.         txtLongitude.Text = "COM Port 已關閉";   
  108.         txtQuality.Text = "無";   
  109.         txtSTQty.Text = "0";   
  110.         txtSeaLevel.Text = "0";   
  111.         btn_upload.Enabled = false;   
  112.     }   
  113. }  

以上的Code就完成了GPS的訊號抓取,轉換,呈現與回報資料庫的動作了,當然,一定可以寫的更好,做更多的運用,使用更多來自衛星的訊息,而這些只是用來實作出用VS2005 C#來達成這個想法,所以看起來較簡略,其它就依各自的需求與想法去延伸.當初花了一點時間在找經緯度轉換的公式,而這也是這整個的關鍵所在,所以這一整個程式碼部份,光看GPS訊號相關,其實就從Serial Port抓訊息,轉換與呈現,原本以為很複雜,結果還好,很簡單,下篇文章就是如何運用google API來呈現所回傳的所在位置.

 

 

===== 2010/3/24 補充 =====

在這段時間回覆大家所提出的問題,這裡整理一下最常被問到的問題與解答 :

Q1. 顯示定位資料不足,而GPS資料長的像是 :&GPGGA,235947,031,,,,,0,00,,,M,0,0,M,,00005A

A1. 這個是因為GPS尚未定好位,就像GPS導航系統的冷開機一樣,是需要花上一點時間,至於要多久,就不一定了,這個是由GPS接收IC來決定的,而最重要的一點,請在收的到衛星的地方測.

 

Q2. 為什麼用別的軟體都抓的到,用這邊提供的Sample Code就抓不到,把別的軟體關掉,這個Sample Code才抓的到.

A2. 因為這個Port被獨佔了,所以別的軟體無法讀取這個Port. 如果可以共用讀取同一個Port,在WM6 SDK內有範例.

 

Q3. Com Port設定是對的,也確定沒有別的程式在讀這個Port,為什麼會抓不到資料或是亂碼.

A3. 請調整SerialPort1.BaudRate,一般設9600就好,再往上調,並不會定會的比較快.

 

Q4. 為何座標的小數點要除以60

A4. 1度=60分,1分=60秒. 參考 Wiki 經緯度

 

Q5. 如何確實停止GPS服務,而不是只關閉GPS連線.

A5. 下載WM6的SDK,裡面有一個Sample Code就有GPS的Open及Close.在安裝完SDK後,範例預設在這個路徑下.

C:\Program Files\Windows Mobile 6 SDK\Samples\PocketPC\CS\GPS

 

參考資料 :

GPS - NMEA sentence Information

NMEA Data

Using the FakeGPS Utility

GPS Intermediate Driver

WM6 SDK

 

原始碼 :

PDA : GPS.zip

Web Service : GPSWS.zip