最近接触了一个项目,主要的使用场景是没有互联网的,所以需要App与北斗卫星进行通信,包括获取地理信息,上报信息,解析后台通过卫星下发的信息。北斗海聊官方只提供了PC版的测试软件,我不知使用了什么方法去查看了他们的源码,没有发现对底层通信协议单独做的封装。网上能查到的都没法用,所以,只能自己从0开始了。
硬件设备
如图,就是这样一个灯罩状的设备,里面插了一张北斗SIM卡。
PC端测试软件
通过USB口连接设备,重新选择端口和波特率,点击端口号旁边的连接,正常情况下就ok了:
此时就可以发一些指令来测试软件和硬件是否正常工作了。
我发送了两条指令,分别是IC读取和定位申请,即上图中的红框1,2。
红框3是发送的指令,红框4是收到的指令,具体的协议我们先跳过,后面再研究。
USB转串口通信
手机可以用无线和有线两种方式与设备进行通信,我们选择的有线的方式,所以使用USB转串口通信。这部分推荐Android usb及串口通信,我也是使用博主的工具进行调试的,调通之后再开始接入自己的项目,进行后续开发。我针对北斗海聊做的一些特殊的优化在这个项目中。
北斗协议
开发文档我先后拿到过三份,都不尽相同。推荐新手从开发快速入门手册开始看,能够比较快的上手。我最开始拿到的是页数最多的那份,当时心里真是...ε=(´ο`*)))。
标准:
$IDsss,d1,d2,……,dn*hh<CR><LF>
一些典型的指令:
$CCICA,0,00*7B\r\n
$CCRMO,GGA,2,60*09\r\n
$BDFKI,DWA,Y,Y,0,0000*0C\n
- $
一句指令的开始。 - ID
这里只是一个标识符,不要给后面出现的用户ID混淆。发送给设备的指令为CC,收到设备返回的指令为BD。 - sss
这是具体指令的名字。 - ...
一直到*号之前,这就是具体的指令内容了。 - *号
分割符,前面是具体指令内容,后面两位就异或校验。 - <CR><LF>
回车换行符,不同平台有所区别,Android平台是\r\n。这是一条指令的终止符,很重要!!!
Talk is cheap,show me the code
- IC读取
/**
* 读取卡号
*
* @return 读取卡号命令
*/
public static String getICCmd() {
return "$CCICA,0,00*7B\r\n";
}
- 获取地理信息
/**
* 获取位置信息,北斗一代
*
* @return 获取位置信息命令
*/
public static String getLocationCmdV1() {
return "$CCDWA,0000000,V,1,L,,0,,,0*65\r\n";
}
一代的定位精度低一些,现在一般都不用了。
/**
* 获取位置信息,北斗二代,更加精确,频度60s
*
* @return 获取位置信息命令
*/
public static String getLocationCmd() {
return "$CCRMO,GGA,2,60*09\r\n";
}
public static String getLocationCmd(int freq) {
String s = "CCRMO,GGA,2," + freq;
String check = SerialPortUtil.getBCC(s.getBytes());
return "$" + s + "*" + check + "\r\n";
}
北斗二代获取地理信息有频度限制,最高60s/次。
- 停止输出
/**
* 停止输出所有指令
*
* @return
*/
public static String stopOutputCmd() {
return "$CCRMO,,3,*4F" + "\r\n";
}
比如60s/次开始定位后,想通过不断电的方式让设备停止定位,则可以发送此指令。
- 发送短报文
/**
* 发送短报文
*
* @param id 收信方用户id,必须为7位,eg:0967760
* @param content 短报文内容
* @return 发送短报文命令,eg:
* $CCTXA,0967760,1,2,A43132335F414243BABAD7D6*77,其内容为”123_ABC汉字“
*/
public static String getMsgCmd(String id, String content) {
String contentFlag = "A4";
String start = "CCTXA";
//分别表示通信类别和传输方式,这里选择了普通通信、混合传输
String middle = "1,2";
String result = null;
try {
String charsetName = "gb2312";
byte[] contentBytes = content.getBytes(charsetName);
StringBuilder sb = new StringBuilder(start)
.append(",").append(id)
.append(",").append(middle)
.append(",").append(contentFlag);
String hexString = SerialPortUtil.encodeHexString(contentBytes);
sb.append(hexString);
String s = sb.toString();
String check = SerialPortUtil.getBCC(s.getBytes(charsetName));
result = "$" + s + "*" + check + "\r\n";
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return result;
}
- 解析反馈信息
/**
* 解析反馈信息
*
* @param response 反馈字符串,eg:$BDFKI,TXA,Y,Y,0,0000*13,
* $BDFKI,DWA,N,Y,0,0058*16
* @return
*/
public static BeidouBean.Response parseResponse(String response) {
String[] split = response.split(",");
BeidouBean.Response res = new BeidouBean.Response();
res.cmdName = split[1];
res.success = "Y".equals(split[2]);
res.freqSetting = "Y".equals(split[3]);
res.limitStatus = Integer.parseInt(split[4]);
String hourSecond = split[5];
String hour = hourSecond.substring(0, 2);
String second = hourSecond.substring(2, 4);
res.waitSecond = Integer.parseInt(hour) * 60 +
Integer.parseInt(second);
return res;
}
//发出指令后的反馈信息
public static class Response{
public String cmdName;
//指令是否执行成功
public boolean success;
public boolean freqSetting;
//0-发射抑制解除,大于0则不正常
public int limitStatus;
//当用户设备发送入站申请时,若距离上一次入站申请
//的时间间隔小于服务频度时,给出等待时间提示,格式为hhss
public int waitSecond;
}
- 解析地理信息
/**
* 解析位置信息,用于北斗一代
*
* @param response 回传字符串,eg:
* $BDDWR,1,0242407,084936.50,2302.2434,N,11323.6667,E,14,M,-6,M,1,V,V,L*1F
* @return
*/
public static BeidouBean.Location parseLocationV1(String response) {
String[] split = response.split(",");
BeidouBean.Location location = new BeidouBean.Location();
location.customStr = response;
location.userId = split[2];
location.time = split[3];
location.lat = split[4];
location.latDirection = split[5];
location.lon = split[6];
location.lonDirection = split[7];
location.altitude = split[8];
return location;
}
/**
* 解析位置信息
*
* @param response 回传字符串,eg:
* $GNGGA,063846.00,2914.96875,N,10444.57129,E,1,12,1.07,316.47,M,0,M,,,2.58*6A
* @return
*/
public static BeidouBean.Location parseLocation(String response) {
String[] split = response.split(",");
BeidouBean.Location location = new BeidouBean.Location();
location.customStr = response;
location.time = split[1];
location.lat = split[2];
location.latDirection = split[3];
location.lon = split[4];
location.lonDirection = split[5];
location.altitude = split[9];
return location;
}
我封装的数据模型里并没有把所有信息都加进去,大家使用的使用可以自己拓展。另外,以上方法中没有对回传的指令进行异或校验,上生产时应该加上。
- 接收指令
这里有一个坑,一条指令可能会分为2次甚至3次传送回来,所以必须自己做处理。我的解决方案是创建一个buf数组,每次接收到的指令都往里面放,直到读到终止符\r\n。
DeviceMeasureController.INSTANCE.measure(usbSerialPort,
new UsbMeasureParameter(UsbPortDeviceType.USB_OTHERS,
19200, 8, 1, 0), new UsbMeasureListener() {
private byte[] buf = new byte[256];
private int index = 0;
@Override
public void measuring(@NotNull UsbSerialPort usbSerialPort, @NotNull byte[] data) {
XLog.d(Arrays.toString(data));
System.arraycopy(data, 0, buf, index, data.length);
// 换行符
if (data[data.length - 1] == (byte) 10) {
String response = new String(buf, 0, index + data.length);
XLog.d(response);
XLog.d(response.length());
String info;
if (response.startsWith("$BDFKI")) {
BeidouBean.Response bResponse = BeidouUtil.parseResponse(response);
if ("DWA".equals(bResponse.cmdName)) {
if (!bResponse.success) {
//todo
}
} else if ("TXA".equals(bResponse.cmdName)) {
if (bResponse.success) {
//todo
} else {
if (bResponse.waitSecond == 0) {
//todo
} else {
//todo
}
XLog.w(String.format("还需等待%ss", bResponse.waitSecond));
}
}
}
info = bResponse.toString();
} else if (response.startsWith("$GNGGA")) {
String location = BeidouUtil.customLocation(response);
//todo
info = location;
}else if(response.startsWith("$BDTXR")){
//下发
BeidouBean.pushMsg pushMsg = BeidouUtil.parsePushMsg(response);
receiveMsg(pushMsg);
info = pushMsg.toString();
} else {
info = response;
}
XLog.d(info);
XLog.d(Arrays.toString(buf));
buf = new byte[256];
index = 0;
} else {
index += data.length;
}
});
}
@Override
public void write(@NotNull UsbSerialPort usbSerialPort) {
//允许持续性写入数据
try {
usbSerialPort.write(new byte[]{(byte) 0xff, (byte) 0xff}, 1000);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void measureError(@NotNull String message) {
XLog.e(message);
}
});
以上代码看起来很长,因为我把解析不同指令的代码放里面了,简化版本的核心逻辑就这样:
DeviceMeasureController.INSTANCE.measure(usbSerialPort,
new UsbMeasureParameter(UsbPortDeviceType.USB_OTHERS,
19200, 8, 1, 0), new UsbMeasureListener() {
private byte[] buf = new byte[256];
private int index = 0;
@Override
public void measuring(@NotNull UsbSerialPort usbSerialPort, @NotNull byte[] data) {
XLog.d(Arrays.toString(data));
System.arraycopy(data, 0, buf, index, data.length);
// 换行符
if (data[data.length - 1] == (byte) 10) {
String response = new String(buf, 0, index + data.length);
//todo
buf = new byte[256];
index = 0;
} else {
index += data.length;
}
});
}
@Override
public void write(@NotNull UsbSerialPort usbSerialPort) {
//允许持续性写入数据
try {
usbSerialPort.write(new byte[]{(byte) 0xff, (byte) 0xff}, 1000);
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void measureError(@NotNull String message) {
XLog.e(message);
}
});
以上是我这次使用北斗短报文实现的一个伪IM。
代码在此