文章目录
博客资源下载地址 : https://download.csdn.net/download/han1202012/90412654
- 内含 808 协议文档 和 Java 代码
一、协议简介
1、JT/T 808 协议简介
JT/T 808 协议 是中国交通运输部制定的行业标准 , 全称 《 道路运输车辆卫星定位系统北斗兼容车载终端通讯协议技术规范 》 , 该协议规范了道路运输车辆卫星定位系统车载终端与监控平台之间的数据通信和格式,确保信息的准确传输和处理。
2、JT/T 808 协议应用场景
JT/T 808 协议 应用场景 :
- 车辆实时监控: 通过定位终端上报的位置信息,监控中心可以实时了解车辆的位置和行驶状态。
- 紧急调度与救援: 在紧急情况下,通过定位系统快速确定车辆位置,及时进行调度或救援。
- 远程管理与控制: 对车辆进行远程监控和管理,包括远程控制车辆的某些功能。
- 行为分析与驾驶员管理: 通过分析车辆行驶数据,评估驾驶员的驾驶行为,进行安全教育和管理。
- 物流管理: 物流企业可以通过车载终端实时跟踪货物的运输状态和位置信息,优化路线规划,提高运输效率。
- 车辆安防: 通过实时监控和报警功能,为车辆提供了全方位的安全保障,防止事故的发生或减少损失1。
二、协议数据分析
1、数据类型
808 协议的 数据类型 :
上述协议的数据类型 都是采用 大端模式传输 ,
- WORD 类型数据 先传输 高 8 位 , 再传输 低 8 位 ;
- DWORD 先传输高 24 位 ,
2、数据消息结构
808 协议 数据组成 : 808 协议 的 前后 都是 标识位 0x7e , 中间是 消息头、消息体、校验码 ;
下图是更详细的解析 :
3、标志位
前后的 标志位 是 0x7e , 消息头、消息体、校验码 中也可能存在 0x7e 数据 , 如果出现就进行转义处理 ,
- 出现 0x7e 的转义处理 : 0x7e <————> 0x7d 后紧跟一个 0x02;
- 出现 0x7d 的转义处理 : 0x7d <————> 0x7d 后紧跟一个 0x01。
808 协议 的 发送端 , 执行操作 " 消息封装——>计算并填充校验码——>转义 " ;
808 协议 的 接收端 , 执行操作 " 转义还原——>验证校验码——>解析消息 " ;
转义示例 :
- 发送的数据为 : 0x30 0x7e 0x08 0x7d 0x55 ,
- 将数据中的 0x7e 转义为 0x7d 0x02 结果为 : 0x30 0x7d 0x02 0x08 0x7d 0x01 0x55
4、消息头
消息体内容 :
① 消息体属性格式
消息头的 第二部分 第 2 ~ 3 字节 , 显示的是 消息体属性 字段 , 这 2 字节 16 位 的结构如下 :
消息头 中的 消息体属性格式 部分数据解析 :
- 分包位 : 第 13 位 ;
- 该位 为 0 值 时 , 不分包 , 这就是一整包数据 ;
- 该位 为 1 值 时 , 分包 , 分包的详细数据 在 消息头的 消息包封装项 中 ;
- 数据加密方式 : 消息体属性格式 的 第 10 ~ 12 位 是 数据加密方式 位 ;
- 如果这三位都是 0 , 不进行加密 ;
- 第 10 位 为 1 , 消息体 使用 RSA 算法加密 ;
② 消息包封装项
消息头 中的 消息包封装项 : 仅在 消息头 中的 消息体 第 13 位 分包位 为 1 时 生效 ;
③ 消息流水号
用于对发送的消息进行计数 , 从 0 开始计数 ;
每发送一次数据 , 自增 1 ;
开发时 , 将其设置为 静态全局变量即可 , 应用重启后才重置 ;
④ 总结
消息头总结如下 :
5、消息体
不同的 消息 ID , 对应不同的 消息体 数据 ;
如 : 电话回拨 消息 ID 值为 0x8400 , 对应的消息体数据格式如下图所示 ;
终端通用应答 , 平台通用应答 , 消息格式如下 :
6、校验码
从 消息头 第一个字节开始 , 与后面一个字节进行异或操作 , 直到 消息体 的最后一个字节结束 , 得到一个字节的 校验码 , 放在 消息体 后面 ;
三、连接建立与断开 与 终端的注册和鉴权
1、连接建立与断开
客户端 进行 TCP 连接 , 根据 IP 地址 和 端口号 连接服务器 ;
连接后 , 需要先进行 终端注册 , 再进行 终端鉴权 , 然后 服务器 才会处理 该终端的相关数据 , 否则服务器会关闭连接 ;
平台和终端均可根据 TCP 协议主动断开连接,双方都应主动判断 TCP 连接是否断开。
平台判断 TCP 连接断开的方法:
——根据 TCP 协议判断出终端主动断开;
——相同身份的终端建立新连接,表明原连接已断开;
——在一定的时间内未收到终端发出的消息,如终端心跳。 终端判断 TCP 连接断开的方法:
——根据 TCP 协议判断出平台主动断开; ——数据通信链路断开;
——数据通信链路正常,达到重传次数后仍未收到应答。
2、心跳包案例
连接维持 需要 周期性发送 心跳包 , 终端心跳的 消息 ID 为 0x0002 ,
消息体为空 , 需要根据 下图 , 拼接消息头 ;
终端手机号 6 字节 , 假设为 88 88 88 88 88 88
, 消息流水号设置为 03
, 这是发出的第 3 条数据 , 消息体长度为 0 ;
最终拼接的数据为 :
7E 00 02 00 88 88 88 88 88 88 03 校验位 7E
校验位计算代码 : 将 00 02 00 88 88 88 88 88 88 03
数据进行校验计算 ;
// 从消息头开始,同后一字节异或,直到校验码前一个字节,占用一个字节。
public static byte crc(ByteBuf byteBuf) {
ByteBuf buf = byteBuf.copy();
byte checksum = 0;
while (buf.readableBytes() > 0) {
checksum ^= buf.readUnsignedByte();
}
return checksum;
}
3、终端注册与注销
终端 使用前 需要 先注册 , 然后再进行鉴权 , 最后才能正常使用 ;
终端在未注册状态下,应首先进行注册,注册成功后终端将获得鉴权码并进行保存,鉴 权码在终端登录时使用。车辆需要拆除或更换终端前, 终端应该执行注销操作,取消终端和 车辆的对应关系。
终端若选择通过SMS方式发送终端注册和终端注销消息,平台应通过SMS方式发送终端注 册应答对终端注册进行回复,通过SMS方式发送平台通用应答对终端注销进行回复。
上述鉴权码 需要保存在 应用程序 本地数据中 ;
终端执行 注销操作 , 将存储在本地 的 鉴权码 删除 ;
终端注册 , 发送下面格式的消息 :
拼接格式 :
7E(标识位) 消息头 终端注册信息(消息体) 校验位 7E(标识位)
服务器端 会 向 终端发送如下 应答信息 :
4、终端鉴权
终端注册后每次在与平台建立连接后,应立即进行鉴权。鉴权成功前终端不得发送其它 消息。
终端通过发送终端鉴权消息进行鉴权,平台回复平台通用应答消息。
终端鉴权 , 直接把 鉴权码 发送给 服务器端即可 ;
拼接格式 :
7E(标识位) 消息头 鉴权码(消息体) 校验位 7E(标识位)
四、Java 代码参考
参考 https://gitee.com/xuzy001/x808 开源项目 , 里面有一些 解析 808 协议 的 可用的 Java 代码 ;
1、常见的消息类型
将消息类型 , 封装到一个类中 ,
/**
* 808消息类型
*/
public class ProtocolConstant {
/**
* 808协议命令字
* 平台通用应答
*/
public static final int PLATFORM_RESP = 0x8001;
/**
* 808协议命令字
* 设备上报心跳
*/
public static final int HEARTBEAT = 0x0002;
/**
* 808协议命令字
* 平台下发升级
*/
public static final int UPGRADE_REQ = 0x8108;
/**
* 808协议命令字
* 设备升级回复
*/
public static final int UPGRADE_RESP = 0x0108;
/**
* 808协议命令字
* 设备上报版本
*/
public static final int VERSION_4G = 0x0107;
/**
* 808协议命令字
* 平台下发设备透传命令
*/
public static final int PLATFORM_TRANS = 0x07D2;
/**
* 808协议命令字
* 设备上报透传命令
*/
public static final int DEVICE_TRANS = 0x07D3;
/**
* 808协议命令字
* 设备上报位置信息
*/
public static final int LOCATION = 0x0200;
/**
* 透传协议命令字
* 平台下发设备控制
*/
public static final int CONTROL_REQ = 0x0108;
/**
* 透传协议命令字
* 设备控制回复
*/
public static final int CONTROL_RESP = 0x8108;
/**
* 透传协议命令字
* 平台下发参数获取
*/
public static final int GET_PARAM_REQ = 0x0200;
/**
* 透传协议命令字
* 设备参数获取回复
*/
public static final int GET_PARAM_RESP = 0x8200;
/**
* 透传协议命令字
* 平台下发参数批量获取
*/
public static final int GET_PARAM_ALL_REQ = 0x0202;
/**
* 透传协议命令字
* 设备参数批量获取回复
*/
public static final int GET_PARAM_ALL_RESP = 0x8202;
/**
* 透传协议命令字
* 平台下发参设置
*/
public static final int SET_PARAM_REQ = 0x0201;
/**
* 透传协议命令字
* 设备参数设置回复
*/
public static final int SET_PARAM_RESP = 0x8201;
/**
* 透传协议命令字
* 设备上报版本号
*/
public static final int VERSION = 0x8104;
/**
* 透传协议命令字
* 上报imei和iccid
*/
public static final int IMEI_ICCID = 0x8109;
/**
* 透传协议命令字
* 设备上报调试信息
*/
public static final int DEBUG = 0x810F;
/**
* 位置信息命令字
* 辅助刹车信息
*/
public static final int BRAKE = 0x00;
/**
* 位置信息命令字
* 状态信息
*/
public static final int STATE = 0x02;
/**
* 位置信息命令字
* 预警信息
*/
public static final int WARN = 0x07;
/**
* 位置信息命令字
* 急减速信息
*/
public static final int SLOW = 0x09;
/**
* 位置信息命令字
* 报警信息
*/
public static final int ALARM = 0x03;
/**
* 位置信息命令字
* 转弯数据
*/
public static final int TURN = 0x06;
}
2、工具类
该工具类提供了 BCD 码 与 String 字符串相互转化的函数 , 校验码逐位异或函数 , 0x7e 的转义与逆转义 函数 ;
package com.xuzy.x808.common.utils;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
public class ByteUtils {
/**
* 读入数据时通过按位与操作将bcd编码转换成设备编号devId
*
* @param bcd
* @return
*/
public static String bcdToString(byte[] bcd) {
// 存储转码后的字符串
StringBuilder sb = new StringBuilder();
// 循环数组解码 先将每个位上的
for (int i = 0; i < bcd.length; i++) {
// 转换低字节 十六进制的 0x0f 等于十进制的 15,二进制表示为00001111,直接取到低4位
int low = (bcd[i] & 0x0f);
// 转换高字节 十六进制的 0xf0 等于十进制的 240,二进制表示为11110000,右移4位的意思是取高4位
int high = ((bcd[i] & 0xf0) >> 4);
// 如果高字节等于0xf(二进制1111)说明是补的字节,直接抛掉
if (high != 0xf) {
sb.append(high);
}
sb.append(low);
}
// 返回解码字符串
return sb.toString();
}
/**
* 写出数据时将我们业务中的devId转换成bcd编码
*
* @param bcd
* @return
*/
public static byte[] strToBcd(String bcd) {
// 获取字节数组长度
int size = bcd.length() / 2;
int remainder = bcd.length() % 2;
// 存储BCD码字节
byte[] bcdByte = new byte[size + remainder];
// 转BCD码
for (int i = 0; i < size; i++) {
int high = Integer.parseInt(bcd.substring(2 * i, 2 * i + 1));
int low = Integer.parseInt(bcd.substring(2 * i + 1, 2 * i + 2));
bcdByte[i] = (byte) ((high << 4) | low);
}
// 如果存在余数,需要填F
if (remainder > 0) {
int low = Integer.parseInt(bcd.substring(bcd.length() - 1));
bcdByte[bcdByte.length - 1] = (byte) ((0xf << 4) | low);
}
// 返回BCD码字节数组
return bcdByte;
}
/**
* 校验位 校验码指从消息头开始,同后一字节异或,直到校验码前一个字节,占用一个字节。
*
* @return
*/
public static byte crc(ByteBuf byteBuf) {
ByteBuf buf = byteBuf.copy();
byte checksum = 0;
while (buf.readableBytes() > 0) {
checksum ^= buf.readUnsignedByte();
}
return checksum;
}
/*
转义处理
按照808协议中标识位的设计:
采用 0x7e 表示,若校验码、消息头以及消息体中出现 0x7e,则要进行转义处理,转义
规则定义如下:
0x7e <————> 0x7d 后紧跟一个 0x02;
0x7d <————> 0x7d 后紧跟一个 0x01。
转义处理过程如下:
发送消息时:消息封装——>计算并填充校验码——>转义;
接收消息时:转义还原——>验证校验码——>解析消息。
示例:
发送一包内容为 0x30 0x7e 0x08 0x7d 0x55 的数据包,则经过封装如下:0x7e 0x30 7d 0x02 0x08 0x7d
0x01 0x55 0x7e。
<p>
也就是说,我们需要将报文中封装后的数据进行还原,即0x7e 0x30 7d 0x02 0x08 0x7d
0x01 0x55 0x7e 还原为 0x30 0x7e 0x08 0x7d 0x55 的数据包才是我们需要的真实报文
*/
/**
* 输出前,同样要跟输入时一样做个转义
*
* @param byteBuf
* @return
*/
private static ByteBuf transfer0x7e(ByteBuf byteBuf) {
ByteBuf response = Unpooled.buffer(1024);
response.writeByte(0x7e);
for (Integer i = 0; i < byteBuf.readableBytes(); i++) {
byte data = byteBuf.getByte(i);
if (data == 0x7e) {
response.writeByte(0x7d);
response.writeByte(0x02);
} else if (data == 0x7d) {
response.writeByte(0x7d);
response.writeByte(0x01);
} else {
response.writeByte(data);
}
}
response.writeByte(0x7e);
return response;
}
/**
* 还原数据包
*
* @param byteBuf 去除了头尾标识位之后的报文
* @return
*/
public static ByteBuf restore0x7D(ByteBuf byteBuf) {
int length = byteBuf.readableBytes();
int lastIndex = 0;
ByteBuf result = Unpooled.buffer(1024);
for (int i = 0; i < length; i++) {
if (byteBuf.getByte(i) == (byte) 0x7D) {
if (byteBuf.getByte(i + 1) != (byte) 0x01
&& byteBuf.getByte(i + 1) != (byte) 0x02) {
return null;
}
result.writeBytes(byteBuf.slice(lastIndex, i - lastIndex));
lastIndex = i + 2;
if (byteBuf.getByte(i + 1) == (byte) 0x01) {
result.writeByte(0x7D);
}
if (byteBuf.getByte(i + 1) == (byte) 0x02) {
result.writeByte(0x7E);
}
}
}
result.writeBytes(byteBuf.slice(lastIndex, length - 1 - lastIndex + 1));
return result;
}
}
3、消息头封装类
public class MsgHeader {
/**
* 消息Id [0-1] 2byte
*/
private int msgId;
/**
* 【消息体属性】 [2-3] 2byte
*/
private String msgBodyProps;
/***以下16位都是【消息体属性】2字节的内容****/
/**
* 保留位[15-14]
*/
private String reservedBit;
/**
* 是否分包[13] 为1时表示消息体为长消息,进行分包发送处理,具体分包信息看【消息包封装项】。为0,则消息头中无消息包封装项字段
*/
private boolean hasSubPackage;
/**
* 数据加密方式[12-10] 此三位都为0,表示消息体不加密,第10位为1,表示消息体经过 RSA 算法加密
*/
private int encryptionType;
/**
* 消息体长度 [9-0]
*/
private int msgBodyLength;
/***以上16位都是消息体属性2字节的内容****/
/**
* 终端手机号 BCD格式的,[4-9] 6byte
*/
private String devId;
/**
* 流水号 [10-11] 2byte
*/
private int seq;
/***以下32位都是【消息包封装项】4字节的内容,定义分包的信息,如果没有消息包封装项,则从字节12位置开始,就是消息体内容****/
/**
* 消息包总数 [12-13] 2字节
*/
private long totalSubPackage;
/**
* 消息包序号 [14-15] 2字节
*/
private long subPackageSeq;
}
4、拼接数据封装类
public class PackageData {
/**
* 16byte消息头
*/
private MsgHeader msgHeader;
/**
* 消息体字节数组
* 根据具体的实体类设置
*/
private ByteBuf msgBody;
/**
* 校验码 1byte
*/
private int checkCode;
/**
* 将 msgHeader msgBody checkCode 数据先进行转义 然后前后拼接 0x7e
*/
private ByteBuf encodeData;
}
参考资料 :