延續前一篇"讓人知道你在那",本篇將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一定會出錯,由於資料操作並非此次重點,因此先手動輸入兩筆資料進去.
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所提供的資料,在此就不再累述這些訊號一次.
而這次所運用的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 | $GPGGA | Global Positioning System Fix Data |
UTC Time | 095031.254 | 09:50:31 |
Latitude | 2501.9891,N | 北緯 |
Longitude | 12133.8101,E | 東經 |
Fix Quality: | 1 | Data is from a GPS fix |
Number of Satellites | 07 | 7 Satellites are in view |
Horizontal Dilution of Precision (HDOP) | 7.0 | Relative accuracy of horizontal position |
Altitude | 123.9,M | 123.9 meters above mean sea level |
Height of geoid above WGS84 ellipsoid | 15.0,M | 15.0,M |
Time since last DGPS update | 0.0 | 因為採GPS修正,不是DGPS,所以沒有資料 |
DGPS reference station id | 0000 | |
Checksum | *74 | Used 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事件來更新資料
- private void btnStart_Click(object sender, EventArgs e)
- {
- serialPort1.PortName = cmbPort.SelectedItem.ToString();//依下拉選擇的com port設定Serial Port
- try
- {
- serialPort1.Open();//開啟serial port
- txtPortStatus.Text = "";
- }
- catch (Exception ex)
- {
- txtData.Text = ex.Message;
- }
- if (serialPort1.IsOpen)//以下為依port的開關狀態來決定UI的動作
- {
- txtPortStatus.Text = "開啟";
- txtQuality.Text = "無";
- txtSTQty.Text = "0";
- txtSeaLevel.Text = "0";
- cmbPort.Enabled = false;
- btnStart.Enabled = false;
- btnStop.Enabled = true;
- timer1.Enabled = true;
- }
- else
- {
- timer1.Enabled = false;
- txtPortStatus.Text = "關閉";
- txtQuality.Text = "無";
- txtSTQty.Text = "0";
- txtSeaLevel.Text = "0";
- cmbPort.Enabled = true;
- btnStart.Enabled = true;
- btnStop.Enabled = false;
- }
- }
5. 在[結束]的button加入一個click event. 如此,將gps的port關閉
- private void btnStop_Click(object sender, EventArgs e)
- {
- try
- {
- serialPort1.Close();//關閉serial port
- txtPortStatus.Text = "";
- }
- catch (Exception ex)
- {
- txtPortStatus.Text = ex.Message;
- }
- if (serialPort1.IsOpen)//以下為依port的開關狀態來決定UI的動作
- {
- txtPortStatus.Text = "開啟";
- txtQuality.Text = "無";
- txtSTQty.Text = "0";
- txtSeaLevel.Text = "0";
- cmbPort.Enabled = false;
- btnStart.Enabled = false;
- btnStop.Enabled = true;
- timer1.Enabled = true;
- }
- else
- {
- timer1.Enabled = false;
- txtPortStatus.Text = "關閉";
- txtQuality.Text = "無";
- txtSTQty.Text = "0";
- txtSeaLevel.Text = "0";
- cmbPort.Enabled = true;
- btnStart.Enabled = true;
- btnStop.Enabled = false;
- btn_upload.Enabled = false;
- }
- }
6. 在[回報]的Button加入一個click event. 如此回報目前所在位置
- private void btn_upload_Click(object sender, EventArgs e)
- {
- GPSWS.Service1 ws = new GPS.GPSWS.Service1();//將之前做好的WS加入參考使用
- ws.Url="http://localhost/gpsws/service1.asmx";// 設定Web Service的位址
- try
- {
- ws.UpdTrace(1, txtLatitude.Text, txtLongitude.Text);//上傳至資料庫
- }
- catch (Exception ex)
- {
- MessageBox.Show(ex.Message);
- }
- }
7. 在Timer的Tick事件加入判斷從Serial port的GPS訊息,並呈現於UI上
- private void timer1_Tick(object sender, EventArgs e)
- {
- if (serialPort1.IsOpen)
- {
- string GPSData = serialPort1.ReadExisting();//將serialPort所取得的資料存到字串內
- txtData.Text = GPSData;//就是顯示在UI下方的那個大TextBox
- string[] gpsArr = GPSData.Split('$');//依$拆成多個字串陣列
- for (int i = 0; i < gpsArr.Length; i++)
- {
- string strTemp = gpsArr[i];
- string[] lineArr = strTemp.Split(',');
- if (lineArr[0] == "GPGGA")//因為我們只用到GPGGA的訊息,所以其它的訊息不用
- {
- try
- {
- //Latitude
- Double dLat = Convert.ToDouble(lineArr[2]);
- dLat = dLat / 100;
- string[] lat = dLat.ToString().Split('.');
- string la =(((Convert.ToDouble(lat[1]) / 60)*10000)).ToString("#");
- for (int a = 0; a < lat[1].Length; a++)
- {
- if (lat[1].Substring(a, 1) == "0")
- {
- la = "0" + la;
- }
- else
- {
- break;
- }
- }
- Latitude = lat[0].ToString() + "." + la.Substring(0,6);
- //Longitude
- Double dLon = Convert.ToDouble(lineArr[4]);
- dLon = dLon / 100;
- string[] lon = dLon.ToString().Split('.');
- string lo = (((Convert.ToDouble(lon[1]) / 60)*10000)).ToString("#");
- for (int b = 0; b < lon[1].Length; b++)
- {
- if (lon[1].Substring(b, 1) == "0")
- {
- lo = "0" + lo;
- }
- else
- {
- break;
- }
- }
- Longitude = lon[0].ToString() + "." + lo.Substring(0,6);
- //Display
- txtLatitude.Text = Latitude;
- txtLongitude.Text = Longitude;
- txtSTQty.Text = lineArr[7];//衛星數
- txtSeaLevel.Text = lineArr[9];//海平面高度
- switch (lineArr[6])//訊號品質
- {
- case "0":
- txtQuality.Text = "品質太差";
- break;
- case "1":
- txtQuality.Text = "GPS fix(SPS)";
- break;
- case "2":
- txtQuality.Text = "DGPS fix";
- break;
- case "3":
- txtQuality.Text = "PPS fix";
- break;
- case "4":
- txtQuality.Text = "即時性動態測量";
- break;
- case "5":
- txtQuality.Text = "Float RTK";
- break;
- case "6":
- txtQuality.Text = "Estimated";
- break;
- case "7":
- txtQuality.Text = "手動輸入模式";
- break;
- case "8":
- txtQuality.Text = "Simulation mode";
- break;
- default:
- txtQuality.Text = "無";
- break;
- }
- btn_upload.Enabled = true;
- }
- catch
- {
- txtLatitude.Text = "GPS 訊號不足";
- txtLongitude.Text = "GPS 訊號不足";
- txtQuality.Text = "無";
- txtSTQty.Text = "0";
- txtSeaLevel.Text = "0";
- btn_upload.Enabled = false;
- }
- }
- }
- }
- else
- {
- txtLatitude.Text = "COM Port 已關閉";
- txtLongitude.Text = "COM Port 已關閉";
- txtQuality.Text = "無";
- txtSTQty.Text = "0";
- txtSeaLevel.Text = "0";
- btn_upload.Enabled = false;
- }
- }
以上的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
原始碼 :
PDA : GPS.zip
Web Service : GPSWS.zip