Android裝置通過NTRIP協議獲取差分資料實現高精度定位
阿新 • • 發佈:2019-01-03
轉載自http://www.jianshu.com/p/7b93952febc0
專案背景
- 最近在做一個Android的APP專案中有個功能,需要用到Ntrip協議從差分伺服器獲取差分資料,並將差分資料通過藍芽傳送至高精度手持裝置(華信TR502接收機)之後返回固定解的高精度定位資料(NMEA0813協議資料),解出位置資訊後在APP地圖上顯示並描繪運動軌跡並將運動軌跡儲存至手機,最後將獲取的資料(GGA和GST格式的資料)重新封裝新增自己的資訊後實時回傳至伺服器。
- 現將過程中一些程式碼技術做出總結,方便之後記憶和查閱
Ntrip協議從差分伺服器獲取差分資料
Ntrip協議(基於HTTP的應用層RTCM網路傳輸的協議)實際是在TCP/IP協議上進行封裝的,依然使用Socket進行資料通訊,專案中我們直接將獲取到的差分資料封裝進了裝置的BluetoothSocket因此在獲取資料前需要先開始藍芽連線裝置並建立BluetoothSocket。接下來簡述過程和部分程式碼。
- 連線裝置藍芽並建立藍芽資料通道
目前使用手機或平板本身的功能與裝置藍芽初次配對,因此在App裡先進行搜尋已配對的裝置BluetoothAdapter.getDefaultAdapter().getBondedDevices();
點選連線裝置並建立BluetoothSocketbtSocket = btDevice.createRfcommSocketToServiceRecord(uuid); btSocket.connect();
- Ntrip協議獲取差分資料
主業務邏輯程式碼如下
NetWorkService原始碼如下NetWorkServiceNtrip netWorkService=new NetWorkServiceNtrip ( this, ip, port, account, pwd, mountedId, btSocket ); netWorkService.getDifferentialData();
UtilNtrip原始碼如下public class NetWorkServiceNtrip { private static final String CMD_HEAD = "$FCMDB,"; private static final String CMD_END = ",*FF\r\n"; //差分伺服器IP地址 private String mIP; //差分伺服器埠 private String mPort; //使用者名稱 private String mUserID; //密碼 private String mPwd; //掛載點 private String mMountedpoint; //與差分伺服器的Socket private Socket mSocket; //Android裝置藍芽通訊Socket private BluetoothSocket bluetoothSocket; // 與差分伺服器的Socket 的資料輸出流 private DataOutputStream dos; //藍芽通訊的輸出流 private OutputStream btDos; // 與差分伺服器的Socket 的資料輸入流 private DataInputStream dis; //獲取掛載點執行緒 private NetWorkServiceNtrip.UpdateSourceTableThread mUpdateSourceTableThread; private NetWorkServiceNtrip.ReportGGA2Service mReportGGA2Service = null; //獲取差分資料執行緒 private NetWorkServiceNtrip.AcquireDataThread mAcquireDataThread; //獲取掛載點 private ArrayList<String> mountedPoints = null; private String feedBackState = null; //獲取連線狀態 public String getFeedBackState() { return this.feedBackState; } public ArrayList<String> getMountedPoints() { return this.mountedPoints; } private Context mContext; //建構函式 public NetWorkServiceNtrip(Context context,String ipAddress, String port, String userID, String password, String mountedPoint, BluetoothSocket bluetoothSocket) { this.mContext=context; this.mIP = ipAddress; this.mPort = port; this.mUserID = userID; this.mPwd = password; this.mMountedpoint = mountedPoint; this.bluetoothSocket=bluetoothSocket; } //連線差分伺服器並獲取掛載點列表 public synchronized void connect2Server() { if(this.mUpdateSourceTableThread != null) { this.mUpdateSourceTableThread.release(); this.mUpdateSourceTableThread = null; } this.mUpdateSourceTableThread = new NetWorkServiceNtrip.UpdateSourceTableThread((NetWorkServiceNtrip.UpdateSourceTableThread)null); this.mUpdateSourceTableThread.start(); } //連線差分伺服器獲取差分資料 public synchronized void getDifferentialData() { if(this.mAcquireDataThread != null) { this.mAcquireDataThread.cancle(); this.mAcquireDataThread = null; } this.mAcquireDataThread = new NetWorkServiceNtrip.AcquireDataThread((NetWorkServiceNtrip.AcquireDataThread)null); this.mAcquireDataThread.start(); } //連線差分伺服器 private void getCorsServiceSocket(String ip, String port) { try { if(this.mSocket == null) { InetAddress e = Inet4Address.getByName(ip); this.mSocket = new Socket(e, Integer.parseInt(port)); } if(this.dos == null) { this.dos = new DataOutputStream(this.mSocket.getOutputStream()); } if(this.dis == null) { this.dis = new DataInputStream(this.mSocket.getInputStream()); } if(this.bluetoothSocket != null) { this.btDos = this.bluetoothSocket.getOutputStream(); } Log.d("getCorsServiceSocket","Successful"); } catch (UnknownHostException var4) { var4.printStackTrace(); } catch (NumberFormatException var5) { var5.printStackTrace(); } catch (IOException var6) { var6.printStackTrace(); } } //獲取差分資料執行緒 private class AcquireDataThread extends Thread { private boolean _run; private byte[] buffer; private AcquireDataThread(AcquireDataThread acquireDataThread) { this._run = true; this.buffer = new byte[256]; } public void run() { if(NetWorkServiceNtrip.this.mSocket != null) { NetWorkServiceNtrip.this.mSocket = null; } try { NetWorkServiceNtrip.this.getCorsServiceSocket(NetWorkServiceNtrip.this.mIP, NetWorkServiceNtrip.this.mPort); if(NetWorkServiceNtrip.this.dos!=null){ //這裡將傳送的請求引數封裝成Ntrip協議格式 NetWorkServiceNtrip.this.dos.write(UtilNtrip.CreateHttpRequsets(NetWorkServiceNtrip.this.mMountedpoint,NetWorkServiceNtrip.this.mUserID,NetWorkServiceNtrip.this.mPwd).getBytes()); } boolean e = true; while(this._run) { if(NetUtils.isConnected(mContext)){ int e1 = NetWorkServiceNtrip.this.dis.read(this.buffer, 0, this.buffer.length); //自己的業務邏輯中將差分資料大小存入了SharePreference中 UserPreferences.getInstance(mContext).setChaFenDataSize(e1); if(e1 >= 1) { String e1x = new String(this.buffer); if(e1x.startsWith("ICY 200 OK")) { if(NetWorkServiceNtrip.this.mReportGGA2Service == null) { NetWorkServiceNtrip.this.mReportGGA2Service = NetWorkServiceNtrip.this.new ReportGGA2Service(NetWorkServiceNtrip.this.dos, (NetWorkServiceNtrip.ReportGGA2Service)null); NetWorkServiceNtrip.this.mReportGGA2Service.start(); } NetWorkServiceNtrip.this.feedBackState = "ICY 200 OK"; } else if(e1x.contains("401 Unauthorized")) { NetWorkServiceNtrip.this.feedBackState = "401 UNAUTHORIZED"; } else { NetWorkServiceNtrip.this.feedBackState = "SUCCESSFUL"; if(NetWorkServiceNtrip.this.btDos != null) { //此處將差分伺服器的資料直接寫入了藍芽的BluetoothSocket中傳送出去 String head = "$FCMDB," + String.valueOf(e1 + 17) + ","; NetWorkServiceNtrip.this.btDos.write(head.getBytes()); NetWorkServiceNtrip.this.btDos.write(this.buffer, 0, e1); Log.d("buffer",UtilNtrip.bytesToHexString(this.buffer)); NetWorkServiceNtrip.this.btDos.write(",*FF\r\n".getBytes()); } } } } } } catch (UnknownHostException var5) { var5.printStackTrace(); } catch (IOException var6) { var6.printStackTrace(); try { NetWorkServiceNtrip.this.dos.close(); NetWorkServiceNtrip.this.dis.close(); } catch (IOException var4) { var4.printStackTrace(); } } } public void cancle() { try { this._run = false; NetWorkServiceNtrip.this.mSocket.close(); } catch (IOException var2) { var2.printStackTrace(); } } } private class ReportGGA2Service extends Thread { private DataOutputStream dos; private boolean _run; private ReportGGA2Service(DataOutputStream dos, ReportGGA2Service reportGGA2Service) { this.dos = null; this._run = false; this.dos = dos; } public void run() { while(!this._run) { try { this.dos.write(Praser.getGGAMsg().getBytes()); Thread.sleep(180000L); } catch (Exception var2) { this.Cancle(); } } } public void Cancle() { try { this._run = true; } catch (Exception var2) { } } } private class UpdateSourceTableThread extends Thread { private UpdateSourceTableThread(UpdateSourceTableThread updateSourceTableThread) { } public void run() { try { NetWorkServiceNtrip.this.getCorsServiceSocket(NetWorkServiceNtrip.this.mIP, NetWorkServiceNtrip.this.mPort); if(NetWorkServiceNtrip.this.dos == null) { NetWorkServiceNtrip.this.mSocket.setSoTimeout(5000); NetWorkServiceNtrip.this.dos = (DataOutputStream)NetWorkServiceNtrip.this.mSocket.getOutputStream(); } NetWorkServiceNtrip.this.dos.write(Util.Request2NtripServer().getBytes()); byte[] e = new byte[1024]; StringBuilder sb = new StringBuilder(); boolean len = true; String sourceString; int var14; while((var14 = NetWorkServiceNtrip.this.dis.read(e, 0, e.length)) != -1) { sourceString = new String(e, 0, var14); sb.append(sourceString); } sourceString = sb.toString(); if(sourceString.startsWith("SOURCETABLE 200 OK")) { ArrayList mountPoints = new ArrayList(); String[] linStrings = sourceString.split("\r\n"); String[] var10 = linStrings; int var9 = linStrings.length; for(int var8 = 0; var8 < var9; ++var8) { String line = var10[var8]; if(line.startsWith("STR")) { String[] dataStrings = line.trim().split(";"); mountPoints.add(dataStrings[1]); } } NetWorkServiceNtrip.this.mountedPoints = mountPoints; } this.release(); NetWorkServiceNtrip.this.mUpdateSourceTableThread = null; } catch (UnknownHostException var12) { var12.printStackTrace(); } catch (IOException var13) { var13.printStackTrace(); } } private void release() { try { if(NetWorkServiceNtrip.this.dos != null) { NetWorkServiceNtrip.this.dos.close(); } if(NetWorkServiceNtrip.this.dis != null) { NetWorkServiceNtrip.this.dis.close(); } if(NetWorkServiceNtrip.this.mSocket != null) { NetWorkServiceNtrip.this.mSocket.close(); } } catch (IOException var2) { var2.printStackTrace(); } } } }
public class UtilNtrip { public static String CreateHttpRequsets(String mountPoint, String userId, String password) { String msg = "GET /" + mountPoint + " HTTP/1.0\r\n"; msg = msg + "User-Agent: NTRIP GNSSInternetRadio/1.4.11\r\n"; msg = msg + "Accept: */*\r\n"; msg = msg + "Connection: close\r\n"; String tempString = userId + ":" + password; byte[] buf = tempString.getBytes(); String code = Base64.encodeToString(buf, 2); msg = msg + "Authorization: Basic " + code + "\r\n"; msg = msg + "\r\n"; return msg; } public static final String bytesToHexString(byte[] bArray) { StringBuffer sb = new StringBuffer(bArray.length); String sTemp; for (int i = 0; i < bArray.length; i++) { sTemp = Integer.toHexString(0xFF & bArray[i]); if (sTemp.length() < 2) sb.append(0); sb.append(sTemp.toUpperCase()); } return sb.toString(); } }
繪製並儲存軌跡、重新封裝資料格式後回傳
獲取到NMEA0831格式資料後,APP的該功能塊主要做了兩個工作一是將軌跡繪製在Shape底圖上,二是將資料拆分並新增上自己的欄位資訊後重新封裝後將資料回傳至遠端伺服器端,該功能塊採用TCP/IP協議進行回傳,繪製運動軌跡我們採用Arcgis for Android的引擎。由於差分資料已經被寫入了BluetoothSocket中傳入手持高精度裝置中(華信TR502接收機),該裝置會自動計算並獲取高精度NMEA0831格式資料,裝置自動通過BluetoothSocket傳輸資料,因此只需要通過讀取BluetoothSocket中的資料就可獲取高精度資訊。讀取藍芽資料主要程式碼
獲取到資料後提取出經緯度資訊呼叫Arcgis for Android的介面繪製運動軌跡,儲存軌跡時呼叫介面將軌跡圖形轉換為Json格式存於本地或資料庫中。try{ inputStream = btSocket.getInputStream(); if (inputStream != null) { BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "ASCII")); while ((line = reader.readLine()) != null) { ... ... //繪製軌跡 drowRoute(double lng, double lat); ... //封裝並回傳資料 sendTCPData(GGAUtils.cutString(msg, GROUP, DEVICE)); ... ... } }catch(IOException e){ e.printStackTrace(); }
drowRoute中的主要程式碼
儲存路徑很簡單的轉化為Json格式儲存在了本地private void drawRoute(double lng, double lat) { //自身的定點陣圖層,不斷重新整理清除之前的定位點 locationLayer.removeAll(); //是否重新開始定位 if (isFristLoaction && lat != 0) { isFristLoaction = !isFristLoaction; //建立Arcgis的點 lastPoint = new Point(lng, lat); //建立Arcgis線條 poly = new Polyline(); //建立Aicgis圖形 polyGraphic = new Graphic(poly, sls); //開始繪製線條的點 poly.startPath(lastPoint); } else { Point wsgpoint = new Point(lng, lat); //投影座標轉換 Point mapPoint = (Point) GeometryEngine.project(wsgpoint, SpatialReference.create(4326), mapView.getSpatialReference()); if (MDistance(lastPoint.getX(), lastPoint.getY(), mapPoint.getX(), mapPoint.getY()) >= 0.05 && MDistance(lastPoint.getX(), lastPoint.getY(), mapPoint.getX(), mapPoint.getY()) <= 20) { pathGraphlayer.removeAll(); //繪製軌跡線條 poly.lineTo(mapPoint); lastPoint = mapPoint; //將線條新增到軌跡圖層上 pathGraphlayer.addGraphic(polyGraphic); //有軌跡則顯示儲存路徑按鈕 if (polyGraphic != null) { pathBtn.post(new Runnable() { @Override public void run() { pathBtn.setVisibility(View.VISIBLE); } }); } //這裡跑的時候發現這個BUG,GraphicsLayer在addGraphic時有長度限制,不知道是版本原因還是什麼 if (pathGraphlayer.getGraphicIDs().length > 8800) { pathGraphlayer.removeAll(); } } locagraphic = new Graphic(mapPoint, locationMS); locationTS = new TextSymbol(15, "latitude:" + lat + "\n" + "longitude:" + lng, Color.BLACK); Graphic locaTSgra = new Graphic(mapPoint, locationTS); locationLayer.addGraphic(locaTSgra); locationLayer.addGraphic(locagraphic); } }
FileUtils.writeStrToFile( new Date().getTime() + "", GeometryEngine.geometryToJson(mapView.getSpatialReference(), polyGraphic.getGeometry()), fileName);
重新封裝並回傳資料
這一部分主要是對收到NMEA0831格式資料進行拆分合並,使用TCP/IP進行回傳。該功能塊只篩選出了GGA和GST協議格式的資料,主要就是使用的是StringBuilder進行字串的一些操作。
回傳資料使用SocketChannel建立通訊通道,連線的建立、重連機制、判斷伺服器是否關閉、傳送資訊或者資料、關閉連線等使用常用的網路程式設計技術。總結
該功能塊還使用了手機或平板自帶的GPS進行軌跡繪製,發現確實差距比較大,這種方式精度很高只有2釐米左右誤差,底圖也是專業的高精度測繪儀器測量出來並做成Shape地圖,作為Android開發者第一次接觸這種裝置,學到了很多。該功能塊主要使用網路程式設計技術,通訊功能比較多,執行緒也比較多,現在通訊的庫比較多導致自身的一些基礎的知識點也沒有完全掌握,Arcgis for Android 還需要不斷學習。