本文学习部标(交通运输部)JT/T 808,并使用 Golang 语言解析。当然,仅使用位置数据进行演示,所以只是一个开端(是否有后续,暂未知)。本文不是科普,因此不会详细列出协议字段说明,可参考文后给出的资料。
本文关注 2013 年版本的 JT/T 808 协议,最新版本是 JT/T 808-2019,由于 2013 年版本资料较多,而笔者目前未有实物验证,故采用之。
协议传输使用大端方式。
数据类型有:BYTE、WORD、DWORD、BYTE[n]
、BCD[n]
、STRING(GBK编码),等。
消息结构为:标识位 消息头 消息体 校验码 标识位
。
一个完整的包使用0x7e标识,即包的第一个字节为0x7e,包的最后一个字节亦为0x7e。包中数据出现0x7e,则需转义。即将0x7e使用0x7d 0x02
替换。这里引入了0x7d,因此该数值也要转义,即将0x7d使用0x7d 0x01
替换。转义后再发送。接收到数据包时,需要进行还原,才能解析。
校验码计算较简单,将前后的标识0x7e及校验码自身去掉,其它数据进行异或计算即可,占一字节。
消息头中的手机号(终端手机号)为 12 字节,如果不足,在前面补 0。
经纬度精度为小数点后6位,即百万分之一度。如 0x021FD934,十进制为 35641652,即表示 35.641652 度。
协议约定缺省使用 TCP 通信方式,不过笔者看过较多的模块一般使用串口或 IIC 通信,内情如何暂不得而知。
消息头
2013 版本消息头为 12 字节或 16 字节,2019 版本多了 5 个字节,1 个字节的协议版本号(初始为 1 ,关键修改递增),终端手机号多了 4 字节的 BCD 码。
工具类:
// 校验码,数据异或
func CheckSum(buff []byte) (ret byte) {
ret = buff[0] // 取第0个,从第1个开始异或
for i := 1; i < len(buff); i++ {
ret = ret ^ buff[i]
}
return
}
// 将收到的报文转义
func DecodeMsg(buff []byte) []byte {
ret := make([]byte, len(buff)) // 保持原长度
i := 0
// j从1开始,表示去掉了头部的0x74,如果传入的不带标识符,可从0开始,长度少1亦然
for j := 1; j < len(buff)-1; j++ {
if j+1 >= len(buff) {
ret[i] = buff[j]
i++
} else {
if buff[j] == 0x7d && buff[j+1] == 0x01 {
ret[i] = 0x7d
i++
j++
} else if buff[j] == 0x7d && buff[j+1] == 0x02 {
ret[i] = 0x7e
i++
j++
} else {
ret[i] = buff[j]
i++
}
}
}
if buff[i] == 0x7e {
i -= 1
}
return ret[:i]
}
// 将发送的报文转义
func EncodeMsg(buff []byte) []byte {
ret := make([]byte, len(buff)*2+2) // 不会超过此处,头尾为2,假设都转义,*2
i := 0
ret[i] = 0x7e
i += 1
for j := 0; j < len(buff); j++ {
if buff[j] == 0x7e {
ret[i] = 0x7d
i += 1
ret[i] = 0x02
i += 1
} else if buff[j] == 0x7d {
ret[i] = 0x7d
i += 1
ret[i] = 0x01
i += 1
} else {
ret[i] = buff[j]
i += 1
}
}
ret[i] = 0x7e
i += 1
return ret[:i]
}
解析示例:
// 检测包是否合法,如果成功,去掉头尾的7e及检验码
// 注:传入此函数的,应该是一个包,不考虑粘包情况
func CheckPacket(bin []byte) ([]byte, error) {
blen := len(bin)
if blen < 13 { // TODO:2019标准比13大
return nil, errors.New("not enough length < 13")
}
if bin[0] != 0x7e && bin[blen-1] != 0x7e {
return nil, errors.New("no 0x7e found")
}
bin = DecodeMsg(bin) // 去掉头尾的0x7e
blen = len(bin) // 重新计算长度
orgChksum := bin[blen-1]
bin = bin[:blen-1] // 去掉校验码,剩下真正数据
cksum := CheckSum(bin)
if cksum != orgChksum {
return nil, errors.New(fmt.Sprintf("Checksum failed calc 0x%x != org 0x%x", cksum, orgChksum))
}
return bin, nil
}
func ParsePacket(bin []byte) (map[string]interface{}, error) {
array := make(map[string]interface{})
bin, err := CheckPacket(bin)
if err != nil {
log.Println("check failed:", err.Error())
return nil, err
}
buf := com.NewBufferReader(bin)
id := buf.ReadUint16BE()
array["id"] = id
var tmp int = 0
tmp = int(buf.ReadUint16BE())
//tmp := buf.ReadUint16BE()
array["datalen"] = int(tmp & 0x3ff)
array["crypt"] = (tmp>>10) & 0x07 // 0:不加密 1:RSA,其它保留
array["split"] = (tmp>>13) & 0x01
// 保留2比特
log.Println("len: ", len(bin))
headLen := 12 // 消息头至少12字节
if array["split"].(int) == 1 { // 分包,分包项共4字节
headLen += 4
array["splittotal"] = buf.ReadUint16BE()
array["splitnum"] = buf.ReadUint16BE()
}
// 消息体 消息头 校验码,即为数据长度
//totalLen := array["datalen"].(int) + headLen + 1
// 解出消息头,才能判断包长度 注:有的包此判断不通过,暂注释
//if totalLen != len(bin) {
// return nil, errors.New(fmt.Sprintf("package length not ok, calc %d != org %d", totalLen, len(bin)))
//}
array["phonenum"] = buf.ReadBCDString(6) // 终端手机号,6个BCD码,实际是12个数字
array["serialno"] = int(buf.ReadUint16BE())
switch id {
case 0x200: // 位置信息汇报
// --- 基本信息
// 报警标志,不同类型,处理方式不同,此处先读取
tmp := buf.ReadUint32BE()
array["alarm"] = tmp
// 解析警告信息
alarmMsg := ""
var j = 0
if (tmp>>0) & 0x01 == 1 {
if j != 0 {
alarmMsg += " "
}
j++
alarmMsg += "紧急报警"
}
if (tmp>>1) & 0x01 == 1 {
if j != 0 {
alarmMsg += " "
}
j++
alarmMsg += "超速报警"
}
// TODO:其它比特的标志判断
array["alarmMsg"] = alarmMsg
tmp= buf.ReadUint32BE()
array["status"] = tmp
// 解析状态标志
if (tmp>>0) & 0x01 == 1 {
array["ACC"] = "on"
} else {
array["ACC"] = "off"
}
// 定位或未定位
if (tmp>>1) & 0x01 == 1 {
array["locate"] = "on"
} else {
array["locate"] = "off"
}
if (tmp>>1) & 0x01 == 1 {
array["locate"] = "on"
} else {
array["locate"] = "off"
}
// 南北纬
if (tmp>>2) & 0x01 == 1 {
array["latflag"] = "south"
} else {
array["latflag"] = "north"
}
// 东西经
if (tmp>>3) & 0x01 == 1 {
array["lonflag"] = "east"
} else {
array["lonflag"] = "west"
}
// TODO:其它比特判断
// 使用的定位系统
if (tmp>>18) & 0x01 == 1 {
array["locatstyle"] = "GPS"
} else if (tmp>>19) & 0x01 == 1{ // 北斗
array["locatstyle"] = "BD"
} else if (tmp>>20) & 0x01 == 1{
array["locatstyle"] = "GLONASS"
} else if (tmp>>21) & 0x01 == 1{
array["locatstyle"] = "Galileo"
}
array["latitude"] = com.ToFixed(buf.ReadUint32BE(), 6) // 纬度 6位小数点
array["longitude"] = com.ToFixed(buf.ReadUint32BE(), 6) // 经度 6位小数点
array["altitude"] = int(buf.ReadUint16BE()) // 海拔,单位为米
array["speed"] = com.ToFixed(buf.ReadUint16BE(), 1) // 速度,1个小数点
array["direction"] = int(buf.ReadUint16BE()) // 方向,正北为0,顺时针算
array["time"] = buf.ReadBCDString(6) // 时间,6个BCD码,实际为12个数字,年月日时分秒,年保留2位
// --- 附加信息 根据剩下的长度判断,读取
leftLength := buf.LeftLength()
for {
if leftLength <= 0 {
break;
}
var aid byte = 0
var aLength int = 0
aid = buf.ReadUint8()
aLength = int(buf.ReadUint8())
leftLength -= aLength
// 逐个ID判断
if aid == 0x01 {
array["mileage"] = com.ToFixed(buf.ReadUint32BE(), 1) // 里程
} else if aid == 0x02 {
array["oil"] = com.ToFixed(buf.ReadUint16BE(), 1) // 油量
} else if aid == 0x03 {
array["apeed1"] = com.ToFixed(buf.ReadUint16BE(), 1) // 行驶记录功能获取的速度
} else if aid == 0x30 {
array["signal"] = int(buf.ReadUint8()) // 无线通信网络信号强度
} else if aid == 0x31 {
array["gnssnum"] = int(buf.ReadUint8()) // GNSS定位卫星数
}
// more...
}
//array["leftdata"] = com.ToHexString(buf.ReadBytesLeft())
case 0x100:
default:
break;
}
log.Printf("array:\n%##v %v\n", array, com.ToHexString(bin))
return array, nil
}
关于数据转义函数的实现,可使用bytes.Buffer{}、WriteByte()
方式,但测试发现较耗时,不知直接使用数组方便。另外,不使用for...range
方式,而是直接使用索引,因为不需要进行拷贝。
读取 1 、2、4 字节函数已封装好,读取 BCD 码及精确计算等函数,也封装好。
解析函数使用 map 存储,根据不同消息类型进行解析赋值。
使用如下位置数据测试:
7E0200003C064808354296023D0000000000080042021FD9340722758000110260013A17082514425701040004329202020000030200002504000000002B0400000000300111310114777E1C007E
代码使用map存储,输出json形式,转换后结果为:
{
"ACC":"off",
"alarm":0x0,
"alarmMsg":"",
"altitude":17,
"apeed1":"0",
"crypt":0,
"datalen":60,
"direction":314,
"gnssnum":20,
"id":0x200,
"latflag":"north",
"latitude":"35.641652",
"locate":"on", // 有定位
"locatstyle":"BD", // 北斗
"lonflag":"west",
"longitude":"119.698816",
"mileage":"27509",
"oil":"0",
"phonenum":"064808354296",
"serialno":573,
"signal":17,
"speed":"60.8",
"split":0,
"status":0x80042,
"time":"170825144257"
}
其它类型的解析,参考手册即可。数据的解析仅是其中一小部分,主要的工作,还是在与模块之间的交互,如心跳、鉴权等。不过这些暂时未涉及。
https://blog.csdn.net/hylexus/article/details/54987786
https://blog.csdn.net/baidu_32523857/article/details/82787485
https://blog.csdn.net/Occidentalior/article/details/73901830
https://github.com/gldsly/jtt808_demo
https://github.com/niuyn/Jt808