北斗系统学习: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 码。

代码实现

源码

工具类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// 校验码,数据异或
func CheckSum(buff []byte) (ret byte) {
trueret = buff[0] // 取第0个,从第1个开始异或
truefor i := 1; i < len(buff); i++ {
truetrueret = ret ^ buff[i]
true}
truereturn
}

// 将收到的报文转义
func DecodeMsg(buff []byte) []byte {
ret := make([]byte, len(buff)) // 保持原长度
truei := 0
// j从1开始,表示去掉了头部的0x74,如果传入的不带标识符,可从0开始,长度少1亦然
truefor j := 1; j < len(buff)-1; j++ {
truetrueif j+1 >= len(buff) {
truetruetrueret[i] = buff[j]
i++
truetrue} else {
truetruetrueif buff[j] == 0x7d && buff[j+1] == 0x01 {
ret[i] = 0x7d
i++
truetruetruetruej++
truetruetrue} else if buff[j] == 0x7d && buff[j+1] == 0x02 {
truetruetruetrueret[i] = 0x7e
i++
truetruetruetruej++
truetruetrue} else {
ret[i] = buff[j]
i++
truetruetrue}
truetrue}
true}
if buff[i] == 0x7e {
i -= 1
}

truereturn ret[:i]
}

// 将发送的报文转义
func EncodeMsg(buff []byte) []byte {
trueret := make([]byte, len(buff)*2+2) // 不会超过此处,头尾为2,假设都转义,*2
truei := 0
ret[i] = 0x7e
i += 1
truefor j := 0; j < len(buff); j++ {
truetrueif buff[j] == 0x7e {
ret[i] = 0x7d
i += 1
ret[i] = 0x02
i += 1
truetrue} else if buff[j] == 0x7d {
truetruetrueret[i] = 0x7d
i += 1
ret[i] = 0x01
i += 1
truetrue} else {
truetruetrueret[i] = buff[j]
i += 1
truetrue}
true}
trueret[i] = 0x7e
i += 1
truereturn ret[:i]
}

解析示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147

// 检测包是否合法
// 注:传入此函数的,应该是一个包,不考虑粘包情况
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 存储,根据不同消息类型进行解析赋值。

示例

使用如下位置数据测试:

1
7E0200003C064808354296023D0000000000080042021FD9340722758000110260013A17082514425701040004329202020000030200002504000000002B0400000000300111310114777E1C007E

输出结果为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"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"
}

未完事宜

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

参考





  • 本文作者:李迟
  • 版权声明:原创文章,版权归署名作者,转载建议注明出处(当然不注明亦可)。
  • 本文链接:/my-study/beidou-system-jtt808.html