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

1,206 阅读5分钟

这是我参与11月更文挑战的第19天,活动详情查看:2021最后一次更文挑战

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

解析示例:

// 检测包是否合法
// 注:传入此函数的,应该是一个包,不考虑粘包情况
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) // 重新计算长度
    cksum := CheckSum(bin[:blen-1]) // 最后一字节是校验码,不用计算
    if cksum != bin[blen-1] {
        return nil, errors.New(fmt.Sprintf("Checksum failed calc 0x%x != org 0x%x", cksum, bin[blen-1]))
    }
    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())
​
    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)
    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 += "超速报警"
        }
        // more...
        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"
        }
        // 使用的定位系统
        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)
        array["longitude"] = com.ToFixed(buf.ReadUint32BE(), 6)
        array["altitude"] = int(buf.ReadUint16BE())
        array["speed"] = com.ToFixed(buf.ReadUint16BE(), 1)
        array["direction"] = int(buf.ReadUint16BE())
        array["time"] = buf.ReadBCDString(6)
​
        // 附加信息
    
    default:
        break;
        
    }
    
    
    log.Printf("array:\n%##v\n", array)
    
    return array, nil
}

源码说明

关于数据转义函数的实现,可使用bytes.Buffer{}、WriteByte()方式,但测试发现较耗时,不知直接使用数组方便。另外,不使用for...range方式,而是直接使用索引,因为不需要进行拷贝。 读取 1 、2、4 字节函数已封装好,读取 BCD 码及精确计算等函数,也封装好。 解析函数使用 map 存储,根据不同消息类型进行解析赋值。

示例

使用如下位置数据测试:

7E0200003C064808354296023D0000000000080042021FD9340722758000110260013A17082514425701040004329202020000030200002504000000002B0400000000300111310114777E1C007E

输出结果为:

{
    "ACC":"off",
    "alarm":0x0,
    "alarmMsg":"",
    "altitude":17,
    "crypt":0,
    "datalen":60,
    "direction":314,
    "id":0x200,
    "latflag":"north",
    "latitude":"35.641652",
    "locate":"on", // 定位
    "locatstyle":"BD", // 使用北斗系统定位
    "lonflag":"west",
    "longitude":"119.698816",
    "phonenum":"064808354296",
    "serialno":573,
    "speed":"60.8",
    "split":0,
    "status":0x80042,
    "time":"170825144257"
}

未完事宜

其它类型的解析,参考手册即可。数据的解析仅是其中一小部分,主要的工作,还是在与模块之间的交互,如心跳、鉴权等。不过这些暂时未涉及。

参考

blog.csdn.net/hylexus/art… blog.csdn.net/baidu_32523… blog.csdn.net/Occidentali… github.com/gldsly/jtt8… github.com/niuyn/Jt808