From 25845fc49d90205e013bcbc2c0c2b8bd8d1e51f3 Mon Sep 17 00:00:00 2001 From: cuteLittleDevil <792192820@qq.com> Date: Mon, 7 Oct 2024 09:50:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E9=80=9A=E7=94=A8?= =?UTF-8?q?=E5=BA=94=E7=AD=94=E7=AD=89=E6=8C=87=E4=BB=A4=E8=A7=A3=E6=9E=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- protocol/jt808/jt808.go | 75 +++++++++++++---------- protocol/model/base_handle.go | 31 ++++++++++ protocol/model/p_0x8001.go | 61 +++++++++++++++++++ protocol/model/p_0x8100.go | 64 ++++++++++++++++++++ protocol/model/parse_test.go | 104 ++++++++++++++++++++++++++++++++ protocol/model/protocol_test.go | 54 +++++++++++++++++ protocol/model/reply_test.go | 86 ++++++++++++++++++++++++++ protocol/model/t_0x0001.go | 53 ++++++++++++++++ protocol/model/t_0x0002.go | 31 ++++++++++ protocol/utils/utils.go | 10 +++ protocol/utils/utils_test.go | 36 +++++++++++ 11 files changed, 573 insertions(+), 32 deletions(-) create mode 100644 protocol/model/base_handle.go create mode 100644 protocol/model/p_0x8001.go create mode 100644 protocol/model/p_0x8100.go create mode 100644 protocol/model/parse_test.go create mode 100644 protocol/model/protocol_test.go create mode 100644 protocol/model/reply_test.go create mode 100644 protocol/model/t_0x0001.go create mode 100644 protocol/model/t_0x0002.go diff --git a/protocol/jt808/jt808.go b/protocol/jt808/jt808.go index 3f4763a..e9eb481 100644 --- a/protocol/jt808/jt808.go +++ b/protocol/jt808/jt808.go @@ -12,47 +12,64 @@ import ( type ( Header struct { - ID uint16 `json:"ID,omitempty"` // 消息ID - Property *BodyProperty `json:"property"` // 消息属性 - // 协议版本 1-2011 2-2013 3-2019 + // ID 消息ID + ID uint16 `json:"ID,omitempty"` + // Property 消息属性 + Property *BodyProperty `json:"property"` + // ProtocolVersion 协议版本 1-2011 2-2013 3-2019 ProtocolVersion consts.ProtocolVersionType `json:"protocolVersion,omitempty"` - // 根据安装后终端自身的手机号转换。手机号不足 12 位,则在前补充数字 + // TerminalPhoneNo 根据安装后终端自身的手机号转换。手机号不足 12 位,则在前补充数字 // 大陆手机号补充数字 0,港澳台则根据其区号进行位数补充 TerminalPhoneNo string `json:"terminalPhoneNo,omitempty"` - // 占用两个字节,为发送信息的序列号,用于接收方检测是否有信息的丢失, + // SerialNumber 占用两个字节,为发送信息的序列号,用于接收方检测是否有信息的丢失, // 上级平台和下级平台接自己发送数据包的个数计数,互不影响。 // 程序开始运行时等于零,发送第一帧数据时开始计数,到最大数后自动归零 - SerialNumber uint16 `json:"serialNumber,omitempty"` - SubPackageSum uint16 `json:"subPackageSum,omitempty"` // 消息总包数 不分包的时候为0 - SubPackageNo uint16 `json:"subPackageNo,omitempty"` // 消息包序号 + SerialNumber uint16 `json:"serialNumber,omitempty"` + // SubPackageSum 消息总包数 不分包的时候为0 + SubPackageSum uint16 `json:"subPackageSum,omitempty"` + // SubPackageNo 消息包序号 + SubPackageNo uint16 `json:"subPackageNo,omitempty"` - PlatformSerialNumber uint16 `json:"platformSerialNumber,omitempty"` // 平台的流水号 - ReplyID uint16 `json:"replyID,omitempty"` // 平台回复的消息ID + // PlatformSerialNumber 平台的流水号 + PlatformSerialNumber uint16 `json:"platformSerialNumber,omitempty"` + // ReplyID 平台回复的消息ID + ReplyID uint16 `json:"replyID,omitempty"` - headEnd int // 请求头结束位置 - bcdTerminalPhoneNo []byte // 设备上传的bcd编码的手机号 + // headEnd 请求头结束位置 + headEnd int + // bcdTerminalPhoneNo 设备上传的bcd编码的手机号 + bcdTerminalPhoneNo []byte } BodyProperty struct { - Version uint8 `json:"version,omitempty"` // 协议版本 - // 分包标识,1:长消息,有分包;2:无分包 + // Version 协议版本 jt808协议的 + Version uint8 `json:"version,omitempty"` + // PacketFragmented 分包标识,1:长消息,有分包;2:无分包 PacketFragmented uint8 `json:"packetFragmented,omitempty"` - // 加密标识,0为不加密 + // EncryptMethod 加密标识,0为不加密 // 当此三位都为 0,表示消息体不加密; // 当第 10 位为 1,表示消息体经过 RSA 算法加密; - EncryptMethod uint8 `json:"encryptMethod,omitempty"` - BodyDayaLen uint16 `json:"bodyDayaLen,omitempty"` // 消息体长度 + EncryptMethod uint8 `json:"encryptMethod,omitempty"` + // BodyDayaLen 消息体长度 + BodyDayaLen uint16 `json:"bodyDayaLen,omitempty"` - attribute uint16 // 消息属性的原始数据 - bit15 byte // 保留位 - bit14 byte // 2013版本的保留 2019版本为1 - isSubPackage bool // 是否分包 + // attribute 消息属性的原始数据 + attribute uint16 + // bit15 保留位 + bit15 byte + // bit14 2013版本的保留 2019版本为1 + bit14 byte + // 是否分包 + isSubPackage bool } JTMessage struct { - Header *Header `json:"header"` - VerifyCode byte `json:"-"` // 校验码 - Body []byte `json:"body"` + // Header 请求头 固定格式的 + Header *Header `json:"header"` + // VerifyCode 校验码 + VerifyCode byte `json:"-"` + // Body 有效数据 + Body []byte `json:"body"` } ) @@ -150,14 +167,8 @@ func (p *BodyProperty) decode(data []byte) { if p.PacketFragmented == 1 { p.isSubPackage = true } - switch (attribute & 0x400) >> 10 { // 第10-12位 加密方式 - case 0: - p.EncryptMethod = 0 // 不加密 - case 1: - p.EncryptMethod = 1 // RSA算法 - default: - } - p.BodyDayaLen = attribute & 0x3FF // 最低10位 消息体长度 3=011 F=1111 + p.EncryptMethod = uint8((attribute & 0x400) >> 10) // 第10-12位 加密方式 0-不加密 1-RSA + p.BodyDayaLen = attribute & 0x3FF // 最低10位 消息体长度 3=011 F=1111 } func (p *BodyProperty) encode() uint16 { diff --git a/protocol/model/base_handle.go b/protocol/model/base_handle.go new file mode 100644 index 0000000..b127bae --- /dev/null +++ b/protocol/model/base_handle.go @@ -0,0 +1,31 @@ +package model + +import ( + "github.com/cuteLittleDevil/go-jt808/protocol/jt808" + "github.com/cuteLittleDevil/go-jt808/shared/consts" +) + +type BaseHandle struct{} + +func (b *BaseHandle) Parse(_ *jt808.JTMessage) error { + return nil +} + +func (b *BaseHandle) HasReply() bool { + return true +} + +func (b *BaseHandle) ReplyBody(jtMsg *jt808.JTMessage) []byte { + head := jtMsg.Header + // 通用应答 + p8001 := &P0x8001{ + RespondSerialNumber: head.SerialNumber, + RespondID: head.ID, + Result: 0x00, // 0-成功 1-失败 2-消息有误 3-不支持 4-报警处理确认 默认成功 + } + return p8001.Encode() +} + +func (b *BaseHandle) ReplyProtocol() uint16 { + return uint16(consts.P8001GeneralRespond) +} diff --git a/protocol/model/p_0x8001.go b/protocol/model/p_0x8001.go new file mode 100644 index 0000000..4aa0879 --- /dev/null +++ b/protocol/model/p_0x8001.go @@ -0,0 +1,61 @@ +package model + +import ( + "encoding/binary" + "fmt" + "github.com/cuteLittleDevil/go-jt808/protocol" + "github.com/cuteLittleDevil/go-jt808/protocol/jt808" + "github.com/cuteLittleDevil/go-jt808/shared/consts" + "strings" +) + +type P0x8001 struct { + // RespondSerialNumber 应答消息流水号 = 这个消息发送时候的流水号 + RespondSerialNumber uint16 + // RespondID 应答消息ID = 这个消息发送时候的ID + RespondID uint16 + // Result 结果 // 0-成功 1-失败 2-消息有误 3-不支持 4-报警处理确认 + Result byte +} + +func (p *P0x8001) Protocol() uint16 { + return uint16(consts.P8001GeneralRespond) +} + +func (p *P0x8001) ReplyProtocol() uint16 { + return 0 +} + +func (p *P0x8001) Parse(jtMsg *jt808.JTMessage) error { + body := jtMsg.Body + if len(body) != 5 { + return protocol.ErrBodyLengthInconsistency + } + p.RespondSerialNumber = binary.BigEndian.Uint16(body[:2]) + p.RespondID = binary.BigEndian.Uint16(body[2:4]) + p.Result = body[4] + return nil +} + +func (p *P0x8001) Encode() []byte { + return []byte{ + byte(p.RespondSerialNumber >> 8), + byte(p.RespondSerialNumber & 0xFF), + byte(p.RespondID >> 8), + byte(p.RespondID & 0xFF), + p.Result, + } +} + +func (p *P0x8001) String() string { + str := "数据体对象:{\n" + str += fmt.Sprintf("\t%s:[%10x]", consts.P8001GeneralRespond, p.Encode()) + return strings.Join([]string{ + str, + fmt.Sprintf("\t[%04x] 应答消息流水号:[%d]", p.RespondSerialNumber, p.RespondSerialNumber), + fmt.Sprintf("\t[%04x] 应答消息ID:[%d]", p.RespondID, p.RespondID), + fmt.Sprintf("\t[%02x] 结果:[%d] 0-成功 1-失败 "+ + "2-消息有误 3-不支持 4-报警处理确认", p.Result, p.Result), + "}", + }, "\n") +} diff --git a/protocol/model/p_0x8100.go b/protocol/model/p_0x8100.go new file mode 100644 index 0000000..8a5373f --- /dev/null +++ b/protocol/model/p_0x8100.go @@ -0,0 +1,64 @@ +package model + +import ( + "encoding/binary" + "fmt" + "github.com/cuteLittleDevil/go-jt808/protocol" + "github.com/cuteLittleDevil/go-jt808/protocol/jt808" + "github.com/cuteLittleDevil/go-jt808/protocol/utils" + "github.com/cuteLittleDevil/go-jt808/shared/consts" + "strings" +) + +type P0x8100 struct { + // RespondSerialNumber 应答消息流水号 = 这个消息发送时候的流水号 + RespondSerialNumber uint16 + // Result 结果 0-成功 1-车辆已被注册 2-数据库中无该车辆 3-终端已被注册 4-数据库中无该终端 + Result byte + // AuthCode 鉴权码 + AuthCode string +} + +func (p *P0x8100) Protocol() uint16 { + return uint16(consts.P8100RegisterRespond) +} + +func (p *P0x8100) ReplyProtocol() uint16 { + return 0 +} + +func (p *P0x8100) Encode() []byte { + code := utils.String2FillingBytes(p.AuthCode, len(p.AuthCode)) + tmp := []byte{ + byte(p.RespondSerialNumber >> 8), + byte(p.RespondSerialNumber & 0xFF), + p.Result, + } + tmp = append(tmp, code...) + return tmp +} + +func (p *P0x8100) Parse(jtMsg *jt808.JTMessage) error { + body := jtMsg.Body + if len(body) < 3 { + return protocol.ErrBodyLengthInconsistency + } + p.RespondSerialNumber = binary.BigEndian.Uint16(body[:2]) + p.Result = body[2] + p.AuthCode = string(body[3:]) + return nil +} + +func (p *P0x8100) String() string { + str := "数据体对象:{\n" + body := p.Encode() + str += fmt.Sprintf("\t注册消息应答:[%x]", body) + return strings.Join([]string{ + str, + fmt.Sprintf("\t[%04x] 应答流水号:[%d]", p.RespondSerialNumber, p.RespondSerialNumber), + fmt.Sprintf("\t[%02x] 结果:[%d] 0-成功 1-失败 "+ + "2-消息有误 3-不支持 4-报警处理确认", p.Result, p.Result), + fmt.Sprintf("\t[%x] 鉴权码:[%s]", body[3:], p.AuthCode), + "}", + }, "\n") +} diff --git a/protocol/model/parse_test.go b/protocol/model/parse_test.go new file mode 100644 index 0000000..4817b55 --- /dev/null +++ b/protocol/model/parse_test.go @@ -0,0 +1,104 @@ +package model + +import ( + "encoding/hex" + "errors" + "github.com/cuteLittleDevil/go-jt808/protocol" + "github.com/cuteLittleDevil/go-jt808/protocol/jt808" + "testing" +) + +func TestParse(t *testing.T) { + type Handler interface { + Parse(*jt808.JTMessage) error + String() string + } + type args struct { + msg string + Handler + body []byte // 用于覆盖率100测试 强制替换了解析正确的body + } + tests := []struct { + name string + fields Handler + args args + }{ + { + name: "T0X0001 终端-通用应答", + args: args{ + msg: "7e000100050123456789017fff007b01c803bd7e", + Handler: &T0x0001{}, + body: []byte{0, 123, 1, 200}, + }, + fields: &T0x0001{ + SerialNumber: 123, + ID: 456, + Result: 3, + }, + }, + { + name: "P0X8001 平台-通用应答", + args: args{ + msg: "7e8001000501234567890100007fff0002008e7e", + Handler: &P0x8001{}, + body: []byte{0, 0, 0, 0}, + }, + fields: &P0x8001{ + RespondSerialNumber: 32767, + RespondID: 2, + Result: 0, + }, + }, + { + name: "P0X8100 终端-注册消息应答", + args: args{ + msg: "7e8100000e01234567890100000000003132333435363738393031377e", + Handler: &P0x8100{}, + body: []byte{0, 0}, + }, + fields: &P0x8100{ + RespondSerialNumber: 0, + Result: 0, + AuthCode: "12345678901", + }, + }, + { + name: "T0X0002 终端-心跳", + args: args{ + msg: "7e0002000001234567890100008a7e", + Handler: &T0x0002{}, + }, + fields: &T0x0002{}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, _ := hex.DecodeString(tt.args.msg) + jtMsg := jt808.NewJTMessage() + if err := jtMsg.Decode(data); err != nil { + t.Errorf("Decode() error = %v", err) + return + } + if err := tt.args.Parse(jtMsg); err != nil { + t.Errorf("Parse() error = %v", err) + return + } + + //fmt.Println(tt.args.Handler.String()) + if tt.args.Handler.String() != tt.fields.String() { + t.Errorf("Parse() want: \n%v\nactual:\n%v", tt.args, tt.fields) + return + } + if tt.args.body != nil { + jtMsg.Body = tt.args.body + if err := tt.args.Parse(jtMsg); err != nil { + if errors.Is(err, protocol.ErrBodyLengthInconsistency) { + return + } + t.Errorf("Parse() error = %v", err) + return + } + } + }) + } +} diff --git a/protocol/model/protocol_test.go b/protocol/model/protocol_test.go new file mode 100644 index 0000000..2812c0f --- /dev/null +++ b/protocol/model/protocol_test.go @@ -0,0 +1,54 @@ +package model + +import ( + "github.com/cuteLittleDevil/go-jt808/shared/consts" + "testing" +) + +func TestReplyProtocol(t *testing.T) { + type Handler interface { + Protocol() uint16 + ReplyProtocol() uint16 + } + tests := []struct { + name string + args Handler + wantProtocol uint16 + wantReplyProtocol consts.PlatformReplyRequest + }{ + { + name: "T0X0001 终端-通用应答", + args: &T0x0001{}, + wantProtocol: uint16(consts.T0001GeneralRespond), + wantReplyProtocol: consts.P8001GeneralRespond, + }, + { + name: "P0X8001 平台-通用应答", + args: &P0x8001{}, + wantProtocol: uint16(consts.P8001GeneralRespond), + wantReplyProtocol: 0, + }, + { + name: "P0X8100 终端-注册消息应答", + args: &P0x8100{}, + wantProtocol: uint16(consts.P8100RegisterRespond), + wantReplyProtocol: 0, + }, + { + name: "T0X0002 终端-心跳", + args: &T0x0002{}, + wantProtocol: uint16(consts.T0002HeartBeat), + wantReplyProtocol: consts.P8001GeneralRespond, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.args.Protocol() != tt.wantProtocol { + t.Errorf("Protocol() = %v, want %v", tt.args.Protocol(), tt.wantProtocol) + } + if tt.args.ReplyProtocol() != uint16(tt.wantReplyProtocol) { + t.Errorf("ReplyProtocol() = %v, want %v", tt.args.ReplyProtocol(), uint16(tt.wantReplyProtocol)) + } + }) + } +} diff --git a/protocol/model/reply_test.go b/protocol/model/reply_test.go new file mode 100644 index 0000000..f3ee425 --- /dev/null +++ b/protocol/model/reply_test.go @@ -0,0 +1,86 @@ +package model + +import ( + "encoding/hex" + "fmt" + "github.com/cuteLittleDevil/go-jt808/protocol/jt808" + "reflect" + "testing" +) + +func TestReply(t *testing.T) { + type Handler interface { + HasReply() bool + ReplyBody(*jt808.JTMessage) []byte + ReplyProtocol() uint16 + } + type args struct { + Handler + msg2011 string + msg2013 string + msg2019 string + } + type want struct { + result2011 string + result2013 string + result2019 string + } + tests := []struct { + name string + args args + want want + }{ + { + // 测试的数据使用terminal.go中的CreateTerminalPackage生成 + // 终端和平台的流水号都使用0 + name: "T0x0002 终端-心跳", + args: args{ + Handler: &T0x0002{}, + msg2013: "7e000200000123456789017fff0a7e", + msg2019: "7e000240000100000000017299841738ffff027e", + }, + want: want{ + result2013: "7e8001000501234567890100007fff0002008e7e", + result2019: "7e8001400501000000000172998417380000ffff000200867e", + }, + }, + { + name: "T0x0001 终端-通用应答", + args: args{ + Handler: &T0x0001{}, + msg2013: "7e000100050123456789017fff007b01c803bd7e", + }, + want: want{ + result2013: "7e8001000501234567890100007fff0001008d7e", + }, + }, + } + checkReplyInfo := func(t *testing.T, msg string, handler Handler, expectedResult string) { + if msg == "" { + return + } + data, _ := hex.DecodeString(msg) + jtMsg := jt808.NewJTMessage() + if err := jtMsg.Decode(data); err != nil { + t.Errorf("Parse() error = %v", err) + return + } + jtMsg.Header.ReplyID = handler.ReplyProtocol() + if ok := handler.HasReply(); !ok { + return + } + body := handler.ReplyBody(jtMsg) + got := jtMsg.Header.Encode(body) + if !reflect.DeepEqual(fmt.Sprintf("%x", got), expectedResult) { + t.Errorf("ReplyInfo() got = [%x]\n want = [%s]", got, expectedResult) + } + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + checkReplyInfo(t, tt.args.msg2011, tt.args.Handler, tt.want.result2011) + checkReplyInfo(t, tt.args.msg2013, tt.args.Handler, tt.want.result2013) + checkReplyInfo(t, tt.args.msg2019, tt.args.Handler, tt.want.result2019) + }) + } +} diff --git a/protocol/model/t_0x0001.go b/protocol/model/t_0x0001.go new file mode 100644 index 0000000..17048e3 --- /dev/null +++ b/protocol/model/t_0x0001.go @@ -0,0 +1,53 @@ +package model + +import ( + "encoding/binary" + "fmt" + "github.com/cuteLittleDevil/go-jt808/protocol" + "github.com/cuteLittleDevil/go-jt808/protocol/jt808" + "github.com/cuteLittleDevil/go-jt808/shared/consts" + "strings" +) + +type T0x0001 struct { + BaseHandle + SerialNumber uint16 `json:"serialNumber"` + ID uint16 `json:"id"` + Result byte `json:"result"` +} + +func (t *T0x0001) Protocol() uint16 { + return uint16(consts.T0001GeneralRespond) +} + +func (t *T0x0001) Parse(jtMsg *jt808.JTMessage) error { + body := jtMsg.Body + if len(body) != 5 { + return protocol.ErrBodyLengthInconsistency + } + t.SerialNumber = binary.BigEndian.Uint16(body[:2]) + t.ID = binary.BigEndian.Uint16(body[2:4]) + t.Result = body[4] + return nil +} + +func (t *T0x0001) Encode() []byte { + data := make([]byte, 5) + binary.BigEndian.PutUint16(data[:2], t.SerialNumber) + binary.BigEndian.PutUint16(data[2:4], t.ID) + data[4] = t.Result + return data +} + +func (t *T0x0001) String() string { + str := "数据体对象:{\n" + str += fmt.Sprintf("\t%s:[%10x]", consts.T0001GeneralRespond, t.Encode()) + return strings.Join([]string{ + str, + fmt.Sprintf("\t[%04x] 应答流水号:[%d]", t.SerialNumber, t.SerialNumber), + fmt.Sprintf("\t[%04x] 应答消息ID:[%d]", t.ID, t.ID), + fmt.Sprintf("\t[%02x] 结果:[%d] 0-成功 1-失败 "+ + "2-消息有误 3-不支持 4-报警处理确认", t.Result, t.Result), + "}", + }, "\n") +} diff --git a/protocol/model/t_0x0002.go b/protocol/model/t_0x0002.go new file mode 100644 index 0000000..6eb362f --- /dev/null +++ b/protocol/model/t_0x0002.go @@ -0,0 +1,31 @@ +package model + +import ( + "fmt" + "github.com/cuteLittleDevil/go-jt808/shared/consts" + "strings" +) + +type T0x0002 struct { + BaseHandle +} + +func (t *T0x0002) Encode() []byte { + return nil +} + +func (t *T0x0002) String() string { + return strings.Join([]string{ + "数据体对象:{", + fmt.Sprintf("\t%s: nil", consts.T0002HeartBeat), + "}", + }, "\n") +} + +func (t *T0x0002) Protocol() uint16 { + return uint16(consts.T0002HeartBeat) +} + +func (t *T0x0002) ReplyProtocol() uint16 { + return uint16(consts.P8001GeneralRespond) +} diff --git a/protocol/utils/utils.go b/protocol/utils/utils.go index 44a1a00..1473cf4 100644 --- a/protocol/utils/utils.go +++ b/protocol/utils/utils.go @@ -68,3 +68,13 @@ func CreateVerifyCode(data []byte) byte { } return code } + +func String2FillingBytes(text string, size int) []byte { + data := []byte(text) + if len(data) < size { + data = append(data, make([]byte, size-len(data))...) + } else if len(data) > size { + data = data[:size] + } + return data +} diff --git a/protocol/utils/utils_test.go b/protocol/utils/utils_test.go index 766a88a..5ff7f1e 100644 --- a/protocol/utils/utils_test.go +++ b/protocol/utils/utils_test.go @@ -104,3 +104,39 @@ func BenchmarkTime2BCD(b *testing.B) { Time2BCD("200707192359") } } + +func TestString2FillingBytes(t *testing.T) { + type args struct { + text string + size int + } + tests := []struct { + name string + args args + want []byte + }{ + { + name: "需要补0的", + args: args{ + text: "1234", + size: 5, + }, + want: []byte{'1', '2', '3', '4', 0}, + }, + { + name: "去掉多余的", + args: args{ + text: "12345", + size: 4, + }, + want: []byte{'1', '2', '3', '4'}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := String2FillingBytes(tt.args.text, tt.args.size); !reflect.DeepEqual(got, tt.want) { + t.Errorf("String2FillingBytes() = %v, want %v", got, tt.want) + } + }) + } +}