參閱:http://www.dotblogs.com.tw/jeff-yeh/archive/2008/06/11/4266.aspx#13956
之前有寫了一篇"讓人知道你在那"的文章,希望能自己寫程式去運用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不應直接寫在這裡面,這部份就依個人環境需求去改,如果再帶下去,文章會又臭又長,如果有需要,再額外開篇來討論.
public int UpdTrace( int UID, string Lat, string Long)
{
string strconn = "Data Source=localhost;Initial Catalog=GPSDB;User Id=GPSUser;Password=gpsuser;"; //ID及PWD隨個人設定.
SqlConnection conn = new SqlConnection(strconn);
string strcmd = "update gpdtrace set Lat=@Lat,Long=@Long,UpdTime=getdate() where UID=@UID";
SqlCommand cmd = new SqlCommand(strcmd, conn);
cmd.Parameters.AddWithValue("@Lat", Lat);
cmd.Parameters.AddWithValue("@Long", Long);
cmd.Parameters.AddWithValue("@UID", UID);
try
{
conn.Open();
return cmd.ExecuteNonQuery();
}
catch (Exception ex)
{
throw new Exception(ex.Message);
}
finally
{
conn.Close();
conn.Dispose();
cmd.Dispose();
}
}
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事件來更新資料
{
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關閉
div style="border-right: #cccccc 1px solid; padding-right: 4px; border-top: #cccccc 1px solid; padding-left: 4px; font-size: 10pt; padding-bottom: 4px; border-left: #cccccc 1px solid; width: 98%; color: #000000; word-break: break-all; line-height: 16px; padding-top: 4px; border-bottom: #cccccc 1px solid; font-family: Verdana,細明體; background-color: #eeeeee"> privatevoidbtnstop_click(objectsender, eventargs e)
span id="CodeFunction1884_expand_text" style="display: inline">{
try
span id="CodeFunction7354_expand_text" style="display: inline">{
serialport1.close();//關閉serial port br /> txtPortStatus.Text = "";
}
catch(Exception ex)
span id="CodeFunction3702_expand_text" style="display: inline">{
txtPortStatus.Text = ex.Message;
}
if(serialPort1.IsOpen)//以下為依port的開關狀態來決定UI的動作 br /> span id="CodeFunction5770_expand_text" style="display: inline">{
txtPortStatus.Text = "開啟"
txtQuality.Text = "無"
txtSTQty.Text = "0"
txtSeaLevel.Text = "0"
cmbPort.Enabled = false
btnStart.Enabled = false
btnStop.Enabled = true
timer1.Enabled = true
}
else
span id="CodeFunction6204_expand_text" style="display: inline">{
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
}
}/div>
6. 在[回報]的Button加入一個click event. 如此回報目前所在位置
div style="border-right: #cccccc 1px solid; padding-right: 4px; border-top: #cccccc 1px solid; padding-left: 4px; font-size: 10pt; padding-bottom: 4px; border-left: #cccccc 1px solid; width: 98%; color: #000000; word-break: break-all; line-height: 16px; padding-top: 4px; border-bottom: #cccccc 1px solid; font-family: Verdana,細明體; background-color: #eeeeee"> privatevoidbtn_upload_Click(objectsender, EventArgs e)
span id="CodeFunction1323_expand_text" style="display: inline">{
GPSWS.Service1 ws = newGPS.GPSWS.Service1(); //將之前做好的WS加入參考使用 br /> ws.Url="http://localhost/gpsws/service1.asmx";//設定Web Service的位址 br /> try
span id="CodeFunction7656_expand_text" style="display: inline">{
ws.UpdTrace(1, txtLatitude.Text, txtLongitude.Text);//上傳至資料庫 br /> }
catch(Exception ex)
span id="CodeFunction2505_expand_text" style="display: inline">{
MessageBox.Show(ex.Message);
}
}/div>
7. 在Timer的Tick事件加入判斷從Serial port的GPS訊息,並呈現於UI上
div style="border-right: #cccccc 1px solid; padding-right: 4px; border-top: #cccccc 1px solid; padding-left: 4px; font-size: 10pt; padding-bottom: 4px; border-left: #cccccc 1px solid; width: 98%; color: #000000; word-break: break-all; line-height: 16px; padding-top: 4px; border-bottom: #cccccc 1px solid; font-family: Verdana,細明體; background-color: #eeeeee"> privatevoidtimer1_Tick(objectsender, EventArgs e)
span id="CodeFunction3491_expand_text" style="display: inline">{
if(serialPort1.IsOpen)
span id="CodeFunction4457_expand_text" style="display: inline">{
stringGPSData = serialPort1.ReadExisting(); //將serialPort所取得的資料存到字串內 br /> txtData.Text = GPSData;//就是顯示在UI下方的那個大TextBox br /> string] gpsArr = GPSData.Split('$');//依$拆成多個字串陣列 br /> for(inti = 0; i < gpsArr.Length; i++)
span id="CodeFunction4543_expand_text" style="display: inline">{
stringstrTemp = gpsArr[i];
string] lineArr = strTemp.Split(',');
if(lineArr[0] == "GPGGA"//因為我們只用到GPGGA的訊息,所以其它的訊息不用 br /> span id="CodeFunction7075_expand_text" style="display: inline">{
try
span id="CodeFunction5683_expand_text" style="display: inline">{
//Latitude 緯度 br /> Double dLat = Convert.ToDouble(lineArr[2]);
dLat = dLat / 100;
string] lat = dLat.ToString().Split('.');
stringla =(((Convert.ToDouble(lat[1]) / 60)*10000)).ToString("#";
for(inta = 0; a < lat[1].Length; a++)
span id="CodeFunction9228_expand_text" style="display: inline">{
if(lat[1].Substring(a, 1) == "0"
span id="CodeFunction4374_expand_text" style="display: inline">{
la = "0"+ la;
}
else
span id="CodeFunction2713_expand_text" style="display: inline">{
break
}
}
Latitude = lat[0].ToString() + "."+ la.Substring(0,6);
//Longitude 經度 br /> Double dLon = Convert.ToDouble(lineArr[4]);
dLon = dLon / 100;
string] lon = dLon.ToString().Split('.');
stringlo = (((Convert.ToDouble(lon[1]) / 60)*10000)).ToString("#";
for(intb = 0; b < lon[1].Length; b++)
span id="CodeFunction9303_expand_text" style="display: inline">{
if(lon[1].Substring(b, 1) == "0"
span id="CodeFunction1344_expand_text" style="display: inline">{
lo = "0"+ lo;
}
else
span id="CodeFunction8133_expand_text" style="display: inline">{
break
}
}
Longitude = lon[0].ToString() + "."+ lo.Substring(0,6);
//Display br /> txtLatitude.Text = Latitude;
txtLongitude.Text = Longitude;
txtSTQty.Text = lineArr[7]; //衛星數 br /> txtSeaLevel.Text = lineArr[9];//海平面高度 br /> switch(lineArr[6])//訊號品質 br /> span id="CodeFunction4900_expand_text" style="display: inline">{
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
span id="CodeFunction7245_expand_text" style="display: inline">{
txtLatitude.Text = "GPS 訊號不足"
txtLongitude.Text = "GPS 訊號不足"
txtQuality.Text = "無"
txtSTQty.Text = "0"
txtSeaLevel.Text = "0"
btn_upload.Enabled = false
}
}
}
}
else
span id="CodeFunction7526_expand_text" style="display: inline">{
txtLatitude.Text = "COM Port 已關閉"
txtLongitude.Text = "COM Port 已關閉"
txtQuality.Text = "無"
txtSTQty.Text = "0"
txtSeaLevel.Text = "0"
btn_upload.Enabled = false
}
}/div>
以上的Code就完成了GPS的訊號抓取,轉換,呈現與回報資料庫的動作了,當然,一定可以寫的更好,做更多的運用,使用更多來自衛星的訊息,而這些只是用來實作出用VS2005 C#來達成這個想法,所以看起來較簡略,其它就依各自的需求與想法去延伸.當初花了一點時間在找經緯度轉換的公式,而這也是這整個的關鍵所在,所以這一整個程式碼部份,光看GPS訊號相關,其實就從Serial Port抓訊息,轉換與呈現,原本以為很複雜,結果還好