Android系统通过NTRIP协议实现高精度定位     DATE: 2019-11-13 09:39

项目背景

  • 最近在做一个Android的APP项目中有个功能,需要用到Ntrip协议从差分服务器获取差分数据,并将差分数据通过蓝牙传送至高精度手持设备(华信TR502接收机)之后返回固定解的高精度定位数据(NMEA0813协议数据),解出位置信息后在APP地图上显示并描绘运动轨迹并将运动轨迹保存至手机,最后将获取的数据(GGA和GST格式的数据)重新封装添加自己的信息后实时回传至服务器。
  • 现将过程中一些代码技术做出总结,方便之后记忆和查阅

Ntrip协议从差分服务器获取差分数据

CORS高精度定位系统
CORS高精度定位系统
 

Ntrip协议(基于HTTP的应用层RTCM网络传输的协议)实际是在TCP/IP协议上进行封装的,依然使用Socket进行数据通信,项目中我们直接将获取到的差分数据封装进了设备的BluetoothSocket因此在获取数据前需要先开始蓝牙连接设备并建立BluetoothSocket。接下来简述过程和部分代码。
  1. 连接设备蓝牙并建立蓝牙数据通道
    目前使用手机或平板本身的功能与设备蓝牙初次配对,因此在App里先进行搜索已配对的设备
    BluetoothAdapter.getDefaultAdapter().getBondedDevices();
    点击连接设备并创建BluetoothSocket

	
  1.  
    btSocket = btDevice.createRfcommSocketToServiceRecord(uuid);
  2.  
    btSocket.connect();
  3. Ntrip协议获取差分数据 主业务逻辑代码如下

	
  1.  
    NetWorkServiceNtrip netWorkService=new NetWorkServiceNtrip (
  2.  
    this,
  3.  
    ip,
  4.  
    port,
  5.  
    account,
  6.  
    pwd,
  7.  
    mountedId,
  8.  
    btSocket
  9.  
    );
  10.  
    netWorkService.getDifferentialData();
  11.  
     
NetWorkService源码如下

	
  1.  
    public class NetWorkServiceNtrip {
  2.  
    private static final String CMD_HEAD = "$FCMDB,";
  3.  
    private static final String CMD_END = ",*FF\r\n";
  4.  
    //差分服务器IP地址
  5.  
    private String mIP;
  6.  
    //差分服务器端口
  7.  
    private String mPort;
  8.  
    //用户名
  9.  
    private String mUserID;
  10.  
    //密码
  11.  
    private String mPwd;
  12.  
    //挂载点
  13.  
    private String mMountedpoint;
  14.  
    //与差分服务器的Socket
  15.  
    private Socket mSocket;
  16.  
    //Android设备蓝牙通信Socket
  17.  
    private BluetoothSocket bluetoothSocket;
  18.  
    // 与差分服务器的Socket 的数据输出流
  19.  
    private DataOutputStream dos;
  20.  
    //蓝牙通信的输出流
  21.  
    private OutputStream btDos;
  22.  
    // 与差分服务器的Socket 的数据输入流
  23.  
    private DataInputStream dis;
  24.  
    //获取挂载点线程
  25.  
    private NetWorkServiceNtrip.UpdateSourceTableThread mUpdateSourceTableThread;
  26.  
    private NetWorkServiceNtrip.ReportGGA2Service mReportGGA2Service = null;
  27.  
    //获取差分数据线程
  28.  
    private NetWorkServiceNtrip.AcquireDataThread mAcquireDataThread;
  29.  
    //获取挂载点
  30.  
    private ArrayList<String> mountedPoints = null;
  31.  
    private String feedBackState = null;
  32.  
    //获取连接状态
  33.  
    public String getFeedBackState() { return this.feedBackState; }
  34.  
    public ArrayList<String> getMountedPoints() { return this.mountedPoints; }
  35.  
    private Context mContext;
  36.  
    //构造函数
  37.  
    public NetWorkServiceNtrip(Context context,String ipAddress, String port, String userID, String password, String mountedPoint, BluetoothSocket bluetoothSocket) {
  38.  
    this.mContext=context;
  39.  
    this.mIP = ipAddress;
  40.  
    this.mPort = port;
  41.  
    this.mUserID = userID;
  42.  
    this.mPwd = password;
  43.  
    this.mMountedpoint = mountedPoint;
  44.  
    this.bluetoothSocket=bluetoothSocket;
  45.  
    }
  46.  
    //连接差分服务器并获取挂载点列表
  47.  
    public synchronized void connect2Server() {
  48.  
    if(this.mUpdateSourceTableThread != null) {
  49.  
    this.mUpdateSourceTableThread.release();
  50.  
    this.mUpdateSourceTableThread = null;
  51.  
    }
  52.  
    this.mUpdateSourceTableThread = new NetWorkServiceNtrip.UpdateSourceTableThread((NetWorkServiceNtrip.UpdateSourceTableThread)null);
  53.  
    this.mUpdateSourceTableThread.start();
  54.  
    }
  55.  
    //连接差分服务器获取差分数据
  56.  
    public synchronized void getDifferentialData() {
  57.  
    if(this.mAcquireDataThread != null) {
  58.  
    this.mAcquireDataThread.cancle();
  59.  
    this.mAcquireDataThread = null;
  60.  
    }
  61.  
    this.mAcquireDataThread = new NetWorkServiceNtrip.AcquireDataThread((NetWorkServiceNtrip.AcquireDataThread)null);
  62.  
    this.mAcquireDataThread.start();
  63.  
    }
  64.  
    //连接差分服务器
  65.  
    private void getCorsServiceSocket(String ip, String port) {
  66.  
    try {
  67.  
    if(this.mSocket == null) {
  68.  
    InetAddress e = Inet4Address.getByName(ip);
  69.  
    this.mSocket = new Socket(e, Integer.parseInt(port));
  70.  
    }
  71.  
    if(this.dos == null) {
  72.  
    this.dos = new DataOutputStream(this.mSocket.getOutputStream());
  73.  
    }
  74.  
    if(this.dis == null) {
  75.  
    this.dis = new DataInputStream(this.mSocket.getInputStream());
  76.  
    }
  77.  
    if(this.bluetoothSocket != null) {
  78.  
    this.btDos = this.bluetoothSocket.getOutputStream();
  79.  
    }
  80.  
    Log.d("getCorsServiceSocket","Successful");
  81.  
    } catch (UnknownHostException var4) {
  82.  
    var4.printStackTrace();
  83.  
    } catch (NumberFormatException var5) {
  84.  
    var5.printStackTrace();
  85.  
    } catch (IOException var6) {
  86.  
    var6.printStackTrace();
  87.  
    }
  88.  
    }
  89.  
    //获取差分数据线程
  90.  
    private class AcquireDataThread extends Thread {
  91.  
    private boolean _run;
  92.  
    private byte[] buffer;
  93.  
    private AcquireDataThread(AcquireDataThread acquireDataThread) {
  94.  
    this._run = true;
  95.  
    this.buffer = new byte[256];
  96.  
    }
  97.  
    public void run() {
  98.  
    if(NetWorkServiceNtrip.this.mSocket != null) {
  99.  
    NetWorkServiceNtrip.this.mSocket = null;
  100.  
    }
  101.  
    try {
  102.  
    NetWorkServiceNtrip.this.getCorsServiceSocket(NetWorkServiceNtrip.this.mIP, NetWorkServiceNtrip.this.mPort);
  103.  
    if(NetWorkServiceNtrip.this.dos!=null){
  104.  
    //这里将发送的请求参数封装成Ntrip协议格式
  105.  
    NetWorkServiceNtrip.this.dos.write(UtilNtrip.CreateHttpRequsets(NetWorkServiceNtrip.this.mMountedpoint,NetWorkServiceNtrip.this.mUserID,NetWorkServiceNtrip.this.mPwd).getBytes());
  106.  
    }
  107.  
    boolean e = true;
  108.  
    while(this._run) {
  109.  
    if(NetUtils.isConnected(mContext)){
  110.  
    int e1 = NetWorkServiceNtrip.this.dis.read(this.buffer, 0, this.buffer.length);
  111.  
    //自己的业务逻辑中将差分数据大小存入了SharePreference中
  112.  
    UserPreferences.getInstance(mContext).setChaFenDataSize(e1);
  113.  
    if(e1 >= 1) {
  114.  
    String e1x = new String(this.buffer);
  115.  
    if(e1x.startsWith("ICY 200 OK")) {
  116.  
    if(NetWorkServiceNtrip.this.mReportGGA2Service == null) {
  117.  
    NetWorkServiceNtrip.this.mReportGGA2Service = NetWorkServiceNtrip.this.new ReportGGA2Service(NetWorkServiceNtrip.this.dos, (NetWorkServiceNtrip.ReportGGA2Service)null);
  118.  
    NetWorkServiceNtrip.this.mReportGGA2Service.start();
  119.  
    }
  120.  
    NetWorkServiceNtrip.this.feedBackState = "ICY 200 OK";
  121.  
    } else if(e1x.contains("401 Unauthorized")) {
  122.  
    NetWorkServiceNtrip.this.feedBackState = "401 UNAUTHORIZED";
  123.  
    } else {
  124.  
    NetWorkServiceNtrip.this.feedBackState = "SUCCESSFUL";
  125.  
    if(NetWorkServiceNtrip.this.btDos != null) {
  126.  
    //此处将差分服务器的数据直接写入了蓝牙的BluetoothSocket中发送出去
  127.  
    String head = "$FCMDB," + String.valueOf(e1 + 17) + ",";
  128.  
    NetWorkServiceNtrip.this.btDos.write(head.getBytes());
  129.  
    NetWorkServiceNtrip.this.btDos.write(this.buffer, 0, e1);
  130.  
    Log.d("buffer",UtilNtrip.bytesToHexString(this.buffer));
  131.  
    NetWorkServiceNtrip.this.btDos.write(",*FF\r\n".getBytes());
  132.  
    }
  133.  
    }
  134.  
    }
  135.  
    }
  136.  
    }
  137.  
    } catch (UnknownHostException var5) {
  138.  
    var5.printStackTrace();
  139.  
    } catch (IOException var6) {
  140.  
    var6.printStackTrace();
  141.  
    try {
  142.  
    NetWorkServiceNtrip.this.dos.close();
  143.  
    NetWorkServiceNtrip.this.dis.close();
  144.  
    } catch (IOException var4) {
  145.  
    var4.printStackTrace();
  146.  
    }
  147.  
    }
  148.  
    }
  149.  
    public void cancle() {
  150.  
    try {
  151.  
    this._run = false;
  152.  
    NetWorkServiceNtrip.this.mSocket.close();
  153.  
    } catch (IOException var2) {
  154.  
    var2.printStackTrace();
  155.  
    }
  156.  
    }
  157.  
    }
  158.  
    private class ReportGGA2Service extends Thread {
  159.  
    private DataOutputStream dos;
  160.  
    private boolean _run;
  161.  
    private ReportGGA2Service(DataOutputStream dos, ReportGGA2Service reportGGA2Service) {
  162.  
    this.dos = null;
  163.  
    this._run = false;
  164.  
    this.dos = dos;
  165.  
    }
  166.  
    public void run() {
  167.  
    while(!this._run) {
  168.  
    try {
  169.  
    this.dos.write(Praser.getGGAMsg().getBytes());
  170.  
    Thread.sleep(180000L);
  171.  
    } catch (Exception var2) {
  172.  
    this.Cancle();
  173.  
    }
  174.  
    }
  175.  
    }
  176.  
    public void Cancle() {
  177.  
    try {
  178.  
    this._run = true;
  179.  
    } catch (Exception var2) {
  180.  
    }
  181.  
    }
  182.  
    }
  183.  
    private class UpdateSourceTableThread extends Thread {
  184.  
    private UpdateSourceTableThread(UpdateSourceTableThread updateSourceTableThread) {
  185.  
    }
  186.  
    public void run() {
  187.  
    try {
  188.  
    NetWorkServiceNtrip.this.getCorsServiceSocket(NetWorkServiceNtrip.this.mIP, NetWorkServiceNtrip.this.mPort);
  189.  
    if(NetWorkServiceNtrip.this.dos == null) {
  190.  
    NetWorkServiceNtrip.this.mSocket.setSoTimeout(5000);
  191.  
    NetWorkServiceNtrip.this.dos = (DataOutputStream)NetWorkServiceNtrip.this.mSocket.getOutputStream();
  192.  
    }
  193.  
    NetWorkServiceNtrip.this.dos.write(Util.Request2NtripServer().getBytes());
  194.  
    byte[] e = new byte[1024];
  195.  
    StringBuilder sb = new StringBuilder();
  196.  
    boolean len = true;
  197.  
    String sourceString;
  198.  
    int var14;
  199.  
    while((var14 = NetWorkServiceNtrip.this.dis.read(e, 0, e.length)) != -1) {
  200.  
    sourceString = new String(e, 0, var14);
  201.  
    sb.append(sourceString);
  202.  
    }
  203.  
    sourceString = sb.toString();
  204.  
    if(sourceString.startsWith("SOURCETABLE 200 OK")) {
  205.  
    ArrayList mountPoints = new ArrayList();
  206.  
    String[] linStrings = sourceString.split("\r\n");
  207.  
    String[] var10 = linStrings;
  208.  
    int var9 = linStrings.length;
  209.  
    for(int var8 = 0; var8 < var9; ++var8) {
  210.  
    String line = var10[var8];
  211.  
    if(line.startsWith("STR")) {
  212.  
    String[] dataStrings = line.trim().split(";");
  213.  
    mountPoints.add(dataStrings[1]);
  214.  
    }
  215.  
    }
  216.  
    NetWorkServiceNtrip.this.mountedPoints = mountPoints;
  217.  
    }
  218.  
    this.release();
  219.  
    NetWorkServiceNtrip.this.mUpdateSourceTableThread = null;
  220.  
    } catch (UnknownHostException var12) {
  221.  
    var12.printStackTrace();
  222.  
    } catch (IOException var13) {
  223.  
    var13.printStackTrace();
  224.  
    }
  225.  
    }
  226.  
    private void release() {
  227.  
    try {
  228.  
    if(NetWorkServiceNtrip.this.dos != null) {
  229.  
    NetWorkServiceNtrip.this.dos.close();
  230.  
    }
  231.  
    if(NetWorkServiceNtrip.this.dis != null) {
  232.  
    NetWorkServiceNtrip.this.dis.close();
  233.  
    }
  234.  
    if(NetWorkServiceNtrip.this.mSocket != null) {
  235.  
    NetWorkServiceNtrip.this.mSocket.close();
  236.  
    }
  237.  
    } catch (IOException var2) {
  238.  
    var2.printStackTrace();
  239.  
    }
  240.  
    }
  241.  
    }
  242.  
    }
  243.  
     
UtilNtrip源码如下

	
  1.  
    public class UtilNtrip {
  2.  
    public static String CreateHttpRequsets(String mountPoint, String userId, String password) {
  3.  
    String msg = "GET /" + mountPoint + " HTTP/1.0\r\n";
  4.  
    msg = msg + "User-Agent: NTRIP GNSSInternetRadio/1.4.11\r\n";
  5.  
    msg = msg + "Accept: */*\r\n";
  6.  
    msg = msg + "Connection: close\r\n";
  7.  
    String tempString = userId + ":" + password;
  8.  
    byte[] buf = tempString.getBytes();
  9.  
    String code = Base64.encodeToString(buf, 2);
  10.  
    msg = msg + "Authorization: Basic " + code + "\r\n";
  11.  
    msg = msg + "\r\n";
  12.  
    return msg;
  13.  
    }
  14.  
    public static final String bytesToHexString(byte[] bArray) {
  15.  
    StringBuffer sb = new StringBuffer(bArray.length);
  16.  
    String sTemp;
  17.  
    for (int i = 0; i < bArray.length; i++) {
  18.  
    sTemp = Integer.toHexString(0xFF & bArray[i]);
  19.  
    if (sTemp.length() < 2)
  20.  
    sb.append(0);
  21.  
    sb.append(sTemp.toUpperCase());
  22.  
    }
  23.  
    return sb.toString();
  24.  
    }
  25.  
    }
  26.  
     

绘制并保存轨迹、重新封装数据格式后回传

获取到NMEA0831格式数据后,APP的该功能块主要做了两个工作一是将轨迹绘制在Shape底图上,二是将数据拆分并添加上自己的字段信息后重新封装后将数据回传至远程服务器端,该功能块采用TCP/IP协议进行回传,绘制运动轨迹我们采用Arcgis for Android的引擎。由于差分数据已经被写入了BluetoothSocket中传入手持高精度设备中(华信TR502接收机),该设备会自动计算并获取高精度NMEA0831格式数据,设备自动通过BluetoothSocket传输数据,因此只需要通过读取BluetoothSocket中的数据就可获取高精度信息。
读取蓝牙数据主要代码

	
  1.  
    try{
  2.  
    inputStream = btSocket.getInputStream();
  3.  
    if (inputStream != null) {
  4.  
    BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, "ASCII"));
  5.  
    while ((line = reader.readLine()) != null) {
  6.  
    ...
  7.  
    ...
  8.  
    //绘制轨迹
  9.  
    drowRoute(double lng, double lat);
  10.  
    ...
  11.  
    //封装并回传数据
  12.  
    sendTCPData(GGAUtils.cutString(msg, GROUP, DEVICE));
  13.  
    ...
  14.  
    ...
  15.  
    }
  16.  
    }catch(IOException e){
  17.  
    e.printStackTrace();
  18.  
    }
  19.  
     
获取到数据后提取出经纬度信息调用Arcgis for Android的接口绘制运动轨迹,保存轨迹时调用接口将轨迹图形转换为Json格式存于本地或数据库中。
drowRoute中的主要代码

	
  1.  
    private void drawRoute(double lng, double lat) {
  2.  
    //自身的定位图层,不断刷新清除之前的定位点
  3.  
    locationLayer.removeAll();
  4.  
    //是否重新开始定位
  5.  
    if (isFristLoaction && lat != 0) {
  6.  
    isFristLoaction = !isFristLoaction;
  7.  
    //创建Arcgis的点
  8.  
    lastPoint = new Point(lng, lat);
  9.  
    //创建Arcgis线条
  10.  
    poly = new Polyline();
  11.  
    //创建Aicgis图形
  12.  
    polyGraphic = new Graphic(poly, sls);
  13.  
    //开始绘制线条的点
  14.  
    poly.startPath(lastPoint);
  15.  
    } else {
  16.  
    Point wsgpoint = new Point(lng, lat);
  17.  
    //投影坐标转换
  18.  
    Point mapPoint = (Point) GeometryEngine.project(wsgpoint, SpatialReference.create(4326), mapView.getSpatialReference());
  19.  
    if (MDistance(lastPoint.getX(), lastPoint.getY(), mapPoint.getX(), mapPoint.getY()) >= 0.05 && MDistance(lastPoint.getX(), lastPoint.getY(), mapPoint.getX(), mapPoint.getY()) <= 20) {
  20.  
    pathGraphlayer.removeAll();
  21.  
    //绘制轨迹线条
  22.  
    poly.lineTo(mapPoint);
  23.  
    lastPoint = mapPoint;
  24.  
    //将线条添加到轨迹图层上
  25.  
    pathGraphlayer.addGraphic(polyGraphic);
  26.  
    //有轨迹则显示保存路径按钮
  27.  
    if (polyGraphic != null) {
  28.  
    pathBtn.post(new Runnable() {
  29.  
    @Override
  30.  
    public void run() {
  31.  
    pathBtn.setVisibility(View.VISIBLE);
  32.  
    }
  33.  
    });
  34.  
    }
  35.  
    //这里跑的时候发现这个BUG,GraphicsLayer在addGraphic时有长度限制,不知道是版本原因还是什么
  36.  
    if (pathGraphlayer.getGraphicIDs().length > 8800) {
  37.  
    pathGraphlayer.removeAll();
  38.  
    }
  39.  
    }
  40.  
    locagraphic = new Graphic(mapPoint, locationMS);
  41.  
    locationTS = new TextSymbol(15, "latitude:" + lat + "\n" + "longitude:" + lng, Color.BLACK);
  42.  
    Graphic locaTSgra = new Graphic(mapPoint, locationTS);
  43.  
    locationLayer.addGraphic(locaTSgra);
  44.  
    locationLayer.addGraphic(locagraphic);
  45.  
    }
  46.  
    }
  47.  
     
保存路径很简单的转化为Json格式保存在了本地

	
  1.  
    FileUtils.writeStrToFile(
  2.  
    new Date().getTime() + "",
  3.  
    GeometryEngine.geometryToJson(mapView.getSpatialReference(), polyGraphic.getGeometry()),
  4.  
    fileName);
  5.  
     
重新封装并回传数据
这一部分主要是对收到NMEA0831格式数据进行拆分合并,使用TCP/IP进行回传。该功能块只筛选出了GGA和GST协议格式的数据,主要就是使用的是StringBuilder进行字符串的一些操作。 回传数据使用SocketChannel建立通信信道,连接的建立、重连机制、判断服务器是否关闭、发送信息或者数据、关闭连接等使用常用的网络编程技术。

总结

该功能块还使用了手机或平板自带的GPS进行轨迹绘制,发现确实差距比较大,这种方式精度很高只有2厘米左右误差,底图也是专业的高精度测绘仪器测量出来并做成Shape地图,作为Android开发者第一次接触这种设备,学到了很多。该功能块主要使用网络编程技术,通信功能比较多,线程也比较多,现在通信的库比较多导致自身的一些基础的知识点也没有完全掌握,Arcgis for Android 还需要不断学习。

链接:https://www.jianshu.com/p/7b93952febc0