北斗系统学习:JTT808协议初步解析

本文学习部标(交通运输部)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

你可能感兴趣的:(技术杂铺)