From 519b30b6381b6bc20f92f9d6859e45a780d7cb0a Mon Sep 17 00:00:00 2001 From: cuteLittleDevil <792192820@qq.com> Date: Mon, 7 Oct 2024 19:28:45 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=B3=A8=E5=86=8C?= =?UTF-8?q?=E7=AD=89=E6=8C=87=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- protocol/go.mod | 5 +- protocol/go.sum | 2 + protocol/model/parse_test.go | 131 +++++++++++++++++- protocol/model/protocol_test.go | 18 +++ protocol/model/reply_test.go | 37 +++++- protocol/model/t_0x0100.go | 162 +++++++++++++++++++++++ protocol/model/t_0x0200.go | 36 +++++ protocol/model/t_0x0200_location_item.go | 76 +++++++++++ protocol/model/t_0x0704.go | 90 +++++++++++++ protocol/utils/utils.go | 71 +++++++--- protocol/utils/utils_test.go | 86 +++++++++++- 11 files changed, 685 insertions(+), 29 deletions(-) create mode 100644 protocol/model/t_0x0100.go create mode 100644 protocol/model/t_0x0200.go create mode 100644 protocol/model/t_0x0200_location_item.go create mode 100644 protocol/model/t_0x0704.go diff --git a/protocol/go.mod b/protocol/go.mod index d554adf..5b88686 100644 --- a/protocol/go.mod +++ b/protocol/go.mod @@ -2,4 +2,7 @@ module github.com/cuteLittleDevil/go-jt808/protocol go 1.23.2 -require github.com/cuteLittleDevil/go-jt808/shared v0.1.0 +require ( + github.com/cuteLittleDevil/go-jt808/shared v0.1.0 + golang.org/x/text v0.19.0 +) diff --git a/protocol/go.sum b/protocol/go.sum index 6bf0305..0f684e5 100644 --- a/protocol/go.sum +++ b/protocol/go.sum @@ -1,2 +1,4 @@ github.com/cuteLittleDevil/go-jt808/shared v0.1.0 h1:Y6uDHbSFqa/VsHcGa713vrB0qz70xidXqDwEkFNe2ro= github.com/cuteLittleDevil/go-jt808/shared v0.1.0/go.mod h1:BMWFmkDRLNjcXcuiPm/yphfWfZ6xNuTAJDkDDNhysOM= +golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM= +golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= diff --git a/protocol/model/parse_test.go b/protocol/model/parse_test.go index 9689ca8..8d9f59b 100644 --- a/protocol/model/parse_test.go +++ b/protocol/model/parse_test.go @@ -100,6 +100,120 @@ func TestParse(t *testing.T) { Version: consts.JT808Protocol2019, }, }, + { + name: "T0x0100 终端注册 2011版本", + args: args{ + msg: "7e010000200123456789010000001f007363640000007777772e3830382e3736353433323101b2e24131323334a17e", + Handler: &T0x0100{}, + bodyLens: []int{24}, + }, + fields: &T0x0100{ + ProvinceID: 31, + CityID: 115, + ManufacturerID: "cd", + TerminalModel: "www.808.", + TerminalID: "7654321", + PlateColor: 1, + LicensePlateNumber: "测A1234", + Version: consts.JT808Protocol2011, + }, + }, + { + name: "T0x0100 终端注册 2013版本", + args: args{ + msg: "7e0100002c0123456789010000001f007363640000007777772e3830382e636f6d0000000000000000003736353433323101b2e24131323334cc7e", + Handler: &T0x0100{}, + bodyLens: []int{36}, + }, + fields: &T0x0100{ + ProvinceID: 31, + CityID: 115, + ManufacturerID: "cd", + TerminalModel: "www.808.com", + TerminalID: "7654321", + PlateColor: 1, + LicensePlateNumber: "测A1234", + Version: consts.JT808Protocol2013, + }, + }, + { + name: "T0x0100 终端注册 2019版本", + args: args{ + msg: "7e0100405301000000000172998417380000001f007363640000000000000000007777772e3830382e636f6d0000000000000000000000000000000000000037363534333231000000000000000000000000000000000000000000000001b2e241313233343b7e", + Handler: &T0x0100{}, + bodyLens: []int{75}, + }, + fields: &T0x0100{ + ProvinceID: 31, + CityID: 115, + ManufacturerID: "cd", + TerminalModel: "www.808.com", + TerminalID: "7654321", + PlateColor: 1, + LicensePlateNumber: "测A1234", + Version: consts.JT808Protocol2019, + }, + }, + { + name: "T0x0200 位置上报", + args: args{ + msg: "7e0200001c0123456789010000000004000000080007203b7d0202633df70138000300632410012359591c7e", + Handler: &T0x0200{}, + bodyLens: []int{27}, + }, + fields: &T0x0200{ + T0x0200LocationItem: T0x0200LocationItem{ + AlarmSign: 1024, + StatusSign: 2048, + Latitude: 119552894, + Longitude: 40058359, + Altitude: 312, + Speed: 3, + Direction: 99, + DateTime: "2024-10-01 23:59:59", + }, + }, + }, + { + name: "T0x0704 位置批量上传", + args: args{ + msg: "7e0704003f0123456789010000000200001c000004000000080007203b7d0202633df7013800030063241001235959001c000004000000080007203b7d0202633df7013800030063241001235959b67e", + Handler: &T0x0704{}, + bodyLens: []int{30, 60, 68}, + }, + fields: &T0x0704{ + Num: 2, + LocationType: 0, + Items: []T0x0704LocationItem{ + { + Len: 28, + T0x0200LocationItem: T0x0200LocationItem{ + AlarmSign: 1024, + StatusSign: 2048, + Latitude: 119552894, + Longitude: 40058359, + Altitude: 312, + Speed: 3, + Direction: 99, + DateTime: "2024-10-01 23:59:59", + }, + }, + { + Len: 28, + T0x0200LocationItem: T0x0200LocationItem{ + AlarmSign: 1024, + StatusSign: 2048, + Latitude: 119552894, + Longitude: 40058359, + Altitude: 312, + Speed: 3, + Direction: 99, + DateTime: "2024-10-01 23:59:59", + }, + }, + }, + }, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -113,7 +227,6 @@ func TestParse(t *testing.T) { 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) @@ -132,3 +245,19 @@ func TestParse(t *testing.T) { }) } } + +// 为了覆盖率100%增加的测试 ------------------------------------ +func TestT0x0704Parse(t *testing.T) { + msg := "7e0704003f0123456789010000000200001c000004000000080007203b7d0202633df7013800030063241001235959001c000004000000080007203b7d0202633df7013800030063241001235959b67e" + data, _ := hex.DecodeString(msg) + jtMsg := jt808.NewJTMessage() + _ = jtMsg.Decode(data) + handler := &T0x0704{} + // 强制错误情况 + jtMsg.Body = jtMsg.Body[:63] + jtMsg.Body[4] = 0x00 + if err := handler.Parse(jtMsg); !errors.Is(err, protocol.ErrBodyLengthInconsistency) { + t.Errorf("T0x0704 Parse() err[%v]", err) + return + } +} diff --git a/protocol/model/protocol_test.go b/protocol/model/protocol_test.go index 095a526..2124920 100644 --- a/protocol/model/protocol_test.go +++ b/protocol/model/protocol_test.go @@ -46,6 +46,24 @@ func TestReplyProtocol(t *testing.T) { wantProtocol: uint16(consts.T0102RegisterAuth), wantReplyProtocol: consts.P8001GeneralRespond, }, + { + name: "T0x0100 终端-注册", + args: &T0x0100{}, + wantProtocol: uint16(consts.T0100Register), + wantReplyProtocol: consts.P8100RegisterRespond, + }, + { + name: "T0x0200 终端-位置上报", + args: &T0x0200{}, + wantProtocol: uint16(consts.T0200LocationReport), + wantReplyProtocol: consts.P8001GeneralRespond, + }, + { + name: "T0x0704 终端-位置批量上传", + args: &T0x0704{}, + wantProtocol: uint16(consts.T0704LocationBatchUpload), + wantReplyProtocol: consts.P8001GeneralRespond, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/protocol/model/reply_test.go b/protocol/model/reply_test.go index 39f763c..0ccf2e7 100644 --- a/protocol/model/reply_test.go +++ b/protocol/model/reply_test.go @@ -68,6 +68,20 @@ func TestReply(t *testing.T) { result2019: "7e80014005010000000001729984173800000000010200877e", }, }, + { + name: "T0x0100 终端-注册", + args: args{ + Handler: &T0x0100{}, + msg2011: "7e010000200123456789010000001f007363640000007777772e3830382e3736353433323101b2e24131323334a17e", + msg2013: "7e0100002c0123456789010000001f007363640000007777772e3830382e636f6d0000000000000000003736353433323101b2e24131323334cc7e", + msg2019: "7e0100405301000000000172998417380000001f007363640000000000000000007777772e3830382e636f6d0000000000000000000000000000000000000037363534333231000000000000000000000000000000000000000000000001b2e241313233343b7e", + }, + want: want{ + result2011: "7e8100000e01234567890100000000003132333435363738393031377e", + result2013: "7e8100000e01234567890100000000003132333435363738393031377e", + result2019: "7e8100400e010000000001729984173800000000003137323939383431373338ba7e", + }, + }, } checkReplyInfo := func(t *testing.T, msg string, handler Handler, expectedResult string) { if msg == "" { @@ -114,10 +128,31 @@ func TestT0x0102Reply(t *testing.T) { } } -func TestT0x002Encode(t *testing.T) { +func TestT0x0002Encode(t *testing.T) { handler := &T0x0002{} got := handler.Encode() if got != nil { t.Errorf("T0x002 Encode() got = [%x]", got) } } + +func TestT0x0200Encode(t *testing.T) { + handler := &T0x0200{ + T0x0200LocationItem: T0x0200LocationItem{ + AlarmSign: 1024, + StatusSign: 2048, + Latitude: 119552894, + Longitude: 40058359, + Altitude: 312, + Speed: 3, + Direction: 99, + DateTime: "2024-10-01 23:59:59", + }, + } + body := handler.Encode() + got := fmt.Sprintf("%x", body) + want := "000004000000080007203b7e02633df7013800030063241001235959" + if got != want { + t.Errorf("T0x0200 Encode() got = %s\n want = %s", got, want) + } +} diff --git a/protocol/model/t_0x0100.go b/protocol/model/t_0x0100.go new file mode 100644 index 0000000..2117b7b --- /dev/null +++ b/protocol/model/t_0x0100.go @@ -0,0 +1,162 @@ +package model + +import ( + "bytes" + "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 T0x0100 struct { + BaseHandle + // ProvinceID 省域 ID + // 标示终端安装车辆所在的省域,0 保留,由平台取默 + // 认值。省域 ID 采用 GB/T 2260 中规定的行政区划代 + // 码六位中前两位 + ProvinceID uint16 `json:"provinceId"` + // CityID 市县域 ID + // 标示终端安装车辆所在的市域和县域,0 保留,由平 + // 台取默认值。市县域 ID 采用 GB/T 2260 中规定的行 + // 政区划代码六位中后四位。 + CityID uint16 `json:"cityId"` + // ManufacturerID 制造商 ID + // 2013版本 5 个字节,终端制造商编码 + // 2019版本 11 个字节,终端制造商编码 + ManufacturerID string `json:"manufacturerId"` + // TerminalModel 终端型号 + // 2011版本 8个字节 ,此终端型号由制造商自行定义,位数不足时,后补“0X00” + // 2013版本 20 个字节,此终端型号由制造商自行定义,位数不足时,后补“0X00”。 + // 2019版本 30 个字节,此终端型号由制造商自行定义,位数不足时,后补“0X00”。 + TerminalModel string `json:"terminalModel"` + // TerminalID 终端 ID + // 2013版本 7个字节,由大写字母和数字组成,此终端 ID 由制造商自行定义,位数不足时,后补“0X00”。 + // 2019版本 30个字节,由大写字母和数字组成,此终端 ID 由制造商自行定义,位数不足时,后补“0X00”。 + TerminalID string `json:"terminalID"` + + // PlateColor 车牌颜色 + // 2013版本 7个字节 按照JT415-2006定义,5.4.12节,0=未上牌,1=蓝,2=黄,3=黑,4=白,9=其他 + // 2019版本 30个字节 按照JT697.7-2014定义,5.6节,0=为上牌,1=蓝,2=黄,3=黑,4=白,5=绿,9=其他 + PlateColor byte `json:"plateColor"` + // LicensePlateNumber 车辆标识 + // 车牌颜色为 0 时,表示车辆 VIN; + // 否则,表示公安交通管理部门颁发的机动车号牌。 + LicensePlateNumber string `json:"licensePlateNumber"` + // Version 版本 1-2011 2-2013 3-2019 + Version consts.ProtocolVersionType `json:"version"` +} + +func (t *T0x0100) Protocol() uint16 { + return uint16(consts.T0100Register) +} + +func (t *T0x0100) Parse(jtMsg *jt808.JTMessage) error { + var ( // 默认按照2011版本 + mLen = 5 + tLen = 8 + tIDLen = 7 + ) + body := jtMsg.Body + switch jtMsg.Header.ProtocolVersion { + case consts.JT808Protocol2019: + mLen = 11 + tLen = 30 + tIDLen = 30 + t.Version = consts.JT808Protocol2019 + case consts.JT808Protocol2013, consts.JT808Protocol2011: + // 根据body长度判断是不是2011版本的 2013版本的车牌颜色是36 + if len(body) > 36 { + t.Version = consts.JT808Protocol2013 + mLen = 5 + tLen = 20 + tIDLen = 7 + } else { + t.Version = consts.JT808Protocol2011 + } + } + + if t.Version == consts.JT808Protocol2011 && len(body) < 25 { + return protocol.ErrBodyLengthInconsistency + } else if t.Version == consts.JT808Protocol2019 && len(body) < 76 { + return protocol.ErrBodyLengthInconsistency + } + + t.ProvinceID = binary.BigEndian.Uint16(body[:2]) + t.CityID = binary.BigEndian.Uint16(body[2:4]) + + cutset := "\x00" + t.ManufacturerID = string(bytes.TrimRight(body[4:4+mLen], cutset)) + t.TerminalModel = string(bytes.TrimRight(body[4+mLen:4+mLen+tLen], cutset)) + t.TerminalID = string(bytes.TrimRight(body[4+mLen+tLen:4+mLen+tLen+tIDLen], cutset)) + t.PlateColor = body[4+mLen+tLen+tIDLen] + utf8Data := utils.GBK2UTF8(body[4+mLen+tLen+tIDLen+1:]) + t.LicensePlateNumber = string(utf8Data) + return nil +} + +func (t *T0x0100) ReplyBody(jtMsg *jt808.JTMessage) ([]byte, error) { + // 不限制 默认鉴权码用手机号 + p8100 := &P0x8100{ + RespondSerialNumber: jtMsg.Header.SerialNumber, + Result: 0, + AuthCode: jtMsg.Header.TerminalPhoneNo, + } + return p8100.Encode(), nil +} + +func (t *T0x0100) Encode() []byte { + mLen, tLen, tIDLen := t.protocolDiff() + data := make([]byte, 4, 4+mLen+tLen+tIDLen) + binary.BigEndian.PutUint16(data[:2], t.ProvinceID) + binary.BigEndian.PutUint16(data[2:4], t.CityID) + data = append(data, utils.String2FillingBytes(t.ManufacturerID, mLen)...) + data = append(data, utils.String2FillingBytes(t.TerminalModel, tLen)...) + data = append(data, utils.String2FillingBytes(t.TerminalID, tIDLen)...) + data = append(data, t.PlateColor) + gbkData := utils.UTF82GBK([]byte(t.LicensePlateNumber)) + data = append(data, gbkData...) + return data +} + +func (t *T0x0100) ReplyProtocol() uint16 { + return uint16(consts.P8100RegisterRespond) +} + +func (t *T0x0100) String() string { + str := "数据体对象:{\n" + data := t.Encode() + str += fmt.Sprintf("\t%s:[%x]", consts.T0100Register, data) + mLen, tLen, tIDLen := t.protocolDiff() + f := func(arg string, size int, remark string) string { + format := "\t[%0" + fmt.Sprintf("%d", mLen) + "x] " + remark + "(%d):[%s]" + return fmt.Sprintf(format, utils.String2FillingBytes(arg, size), size, arg) + } + return strings.Join([]string{ + str, + fmt.Sprintf("\t[%04x] 省域ID:[%d]", t.ProvinceID, t.ProvinceID), + fmt.Sprintf("\t[%04x] 市县域ID:[%d]", t.CityID, t.CityID), + f(t.ManufacturerID, mLen, "制造商ID"), + f(t.TerminalModel, tLen, "终端型号"), + f(t.TerminalID, tIDLen, "终端ID"), + fmt.Sprintf("\t[%02x] 车牌颜色:[%d]", t.PlateColor, t.PlateColor), + fmt.Sprintf("\t[%x] 车牌号:[%s]", data[4+mLen+tLen+tIDLen+1:], t.LicensePlateNumber), + "}", + }, "\n") +} + +func (t *T0x0100) protocolDiff() (int, int, int) { + var ( // 默认按照2011版本 + mLen = 5 + tLen = 8 + tIDLen = 7 + ) + if t.Version == consts.JT808Protocol2013 { + mLen, tLen, tIDLen = 5, 20, 7 + } else if t.Version == consts.JT808Protocol2019 { + mLen, tLen, tIDLen = 11, 30, 30 + } + return mLen, tLen, tIDLen +} diff --git a/protocol/model/t_0x0200.go b/protocol/model/t_0x0200.go new file mode 100644 index 0000000..883244e --- /dev/null +++ b/protocol/model/t_0x0200.go @@ -0,0 +1,36 @@ +package model + +import ( + "fmt" + "github.com/cuteLittleDevil/go-jt808/protocol/jt808" + "github.com/cuteLittleDevil/go-jt808/shared/consts" + "strings" +) + +type T0x0200 struct { + BaseHandle + T0x0200LocationItem +} + +func (t *T0x0200) Protocol() uint16 { + return uint16(consts.T0200LocationReport) +} + +func (t *T0x0200) Parse(jtMsg *jt808.JTMessage) error { + body := jtMsg.Body + return t.parse(body) +} + +func (t *T0x0200) Encode() []byte { + return t.T0x0200LocationItem.encode() +} + +func (t *T0x0200) String() string { + body := t.T0x0200LocationItem.encode() + return strings.Join([]string{ + "数据体对象:{", + fmt.Sprintf("\t%s:[%x]", consts.T0200LocationReport, body), + t.T0x0200LocationItem.String(), + "}", + }, "\n") +} diff --git a/protocol/model/t_0x0200_location_item.go b/protocol/model/t_0x0200_location_item.go new file mode 100644 index 0000000..e2a38e0 --- /dev/null +++ b/protocol/model/t_0x0200_location_item.go @@ -0,0 +1,76 @@ +package model + +import ( + "encoding/binary" + "fmt" + "github.com/cuteLittleDevil/go-jt808/protocol" + "github.com/cuteLittleDevil/go-jt808/protocol/utils" + "strings" +) + +type T0x0200LocationItem struct { + // AlarmSign 报警标志 JT808.Protocol.Enums.JT808Alarm + AlarmSign uint32 `json:"alarmSign"` + // StatusSign 状态标志 JT808.Protocol.Enums.JT808Status + StatusSign uint32 `json:"statusSign"` + // Latitude 纬度 以度为单位的纬度值乘以 10 的 6 次方,精确到百万分之一度 + Latitude uint32 `json:"latitude"` + // Longitude 经度 以度为单位的经度值乘以 10 的 6 次方,精确到百万分之一度 + Longitude uint32 `json:"longitude"` + // Altitude 海拔高度 单位为米 + Altitude uint16 `json:"altitude"` + // Speed 速度 1/10km/h + Speed uint16 `json:"speed"` + // Direction 方向 0-359,正北为 0,顺时针 + Direction uint16 `json:"direction"` + // DateTime 时间 YY-MM-DD-hh-mm-ss(GMT+8 时间,本标准中之后涉及的时间均采用此时区) + DateTime string `json:"dateTime"` +} + +func (tl *T0x0200LocationItem) parse(body []byte) error { + if len(body) < 28 { + return protocol.ErrBodyLengthInconsistency + } + tl.AlarmSign = binary.BigEndian.Uint32(body[:4]) + tl.StatusSign = binary.BigEndian.Uint32(body[4:8]) + tl.Latitude = binary.BigEndian.Uint32(body[8:12]) + tl.Longitude = binary.BigEndian.Uint32(body[12:16]) + tl.Altitude = binary.BigEndian.Uint16(body[16:18]) + tl.Speed = binary.BigEndian.Uint16(body[18:20]) + tl.Direction = binary.BigEndian.Uint16(body[20:22]) + tl.DateTime = utils.BCD2Time(body[22:28]) + return nil +} + +func (tl *T0x0200LocationItem) encode() []byte { + data := make([]byte, 22, 30) + binary.BigEndian.PutUint32(data[:4], tl.AlarmSign) + binary.BigEndian.PutUint32(data[4:8], tl.StatusSign) + binary.BigEndian.PutUint32(data[8:12], tl.Latitude) + binary.BigEndian.PutUint32(data[12:16], tl.Longitude) + binary.BigEndian.PutUint16(data[16:18], tl.Altitude) + binary.BigEndian.PutUint16(data[18:20], tl.Speed) + binary.BigEndian.PutUint16(data[20:22], tl.Direction) + bcdTime := strings.ReplaceAll(tl.DateTime, "-", "") + bcdTime = strings.ReplaceAll(bcdTime, ":", "") + bcdTime = strings.ReplaceAll(bcdTime, " ", "") + if len(bcdTime) == 14 { + bcdTime = bcdTime[2:] + } + data = append(data, utils.Time2BCD(bcdTime)...) + return data +} + +func (tl *T0x0200LocationItem) String() string { + body := tl.encode() + return strings.Join([]string{ + fmt.Sprintf("\t[%08x] 报警标志:[%d]", tl.AlarmSign, tl.AlarmSign), + fmt.Sprintf("\t[%08x] 状态标志:[%d]", tl.StatusSign, tl.StatusSign), + fmt.Sprintf("\t[%08x] 纬度:[%d]", tl.Latitude, tl.Latitude), + fmt.Sprintf("\t[%08x] 经度:[%d]", tl.Longitude, tl.Longitude), + fmt.Sprintf("\t[%04x] 海拔高度:[%d]", tl.Altitude, tl.Altitude), + fmt.Sprintf("\t[%04x] 速度:[%d]", tl.Speed, tl.Speed), + fmt.Sprintf("\t[%04x] 方向:[%d]", tl.Direction, tl.Direction), + fmt.Sprintf("\t[%x] 时间:[%s]", body[22:28], tl.DateTime), + }, "\n") +} diff --git a/protocol/model/t_0x0704.go b/protocol/model/t_0x0704.go new file mode 100644 index 0000000..a90dea4 --- /dev/null +++ b/protocol/model/t_0x0704.go @@ -0,0 +1,90 @@ +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 ( + T0x0704 struct { + BaseHandle + // Num 数据项个数 必须大于0 + Num uint16 `json:"num"` + // LocationType 0-正常位置批量汇报 1-盲区补报 + LocationType byte `json:"locationType"` + // Items 数据项 + Items []T0x0704LocationItem `json:"items"` + } + + T0x0704LocationItem struct { + // Len 长度 位置汇报数据体长度 + Len uint16 `json:"len"` + // T0x0200LocationItem 位置汇报数据体 + T0x0200LocationItem + } +) + +func (t *T0x0704) Protocol() uint16 { + return uint16(consts.T0704LocationBatchUpload) +} + +func (t *T0x0704) Parse(jtMsg *jt808.JTMessage) error { + body := jtMsg.Body + if len(body) < 31 { + return protocol.ErrBodyLengthInconsistency + } + t.Num = binary.BigEndian.Uint16(body[:2]) + t.LocationType = body[2] + start := 3 + for i := 0; i < int(t.Num); i++ { + var item T0x0704LocationItem + item.Len = binary.BigEndian.Uint16(body[start : start+2]) + if start+2+int(item.Len) > len(body) { + return protocol.ErrBodyLengthInconsistency + } + curBody := body[start+2 : start+2+int(item.Len)] + if err := item.T0x0200LocationItem.parse(curBody); err != nil { + return err + } + t.Items = append(t.Items, item) + start += 2 + int(item.Len) + } + return nil +} + +func (t *T0x0704) Encode() []byte { + data := make([]byte, 3, 100) + binary.BigEndian.PutUint16(data[:2], t.Num) + data[2] = t.LocationType + for i := 0; i < len(t.Items); i++ { + body := t.Items[i].encode() + bodyLen := make([]byte, 2) + binary.BigEndian.PutUint16(bodyLen, uint16(len(body))) + data = append(data, bodyLen...) + data = append(data, body...) + } + return data +} + +func (t *T0x0704) String() string { + str := "数据体对象:{\n" + str += fmt.Sprintf("\t%s:[%x]\n", consts.T0704LocationBatchUpload, t.Encode()) + str += fmt.Sprintf("\t[%04x] 数据项个数:[%d]\n", t.Num, t.Num) + str += fmt.Sprintf("\t[%02x] 位置汇报类型:[%d] 0-正常位置批量汇报 1-盲区补报\n", t.LocationType, t.LocationType) + str += fmt.Sprintf("\t位置汇报数据集合: [\n") + for i := 0; i < len(t.Items); i++ { + str += "\t{\n" + itemStr := t.Items[i].String() + str += strings.ReplaceAll(itemStr, "\t", "\t\t") + str += "\n\t}\n" + } + str += "\t]" + return strings.Join([]string{ + str, + "}", + }, "\n") +} diff --git a/protocol/utils/utils.go b/protocol/utils/utils.go index 1473cf4..18ded13 100644 --- a/protocol/utils/utils.go +++ b/protocol/utils/utils.go @@ -2,6 +2,10 @@ package utils import ( "bytes" + "fmt" + "golang.org/x/text/encoding/simplifiedchinese" + "golang.org/x/text/transform" + "io" ) func Bcd2Dec(data []byte) string { @@ -15,9 +19,9 @@ func Bcd2Dec(data []byte) string { } func Time2BCD(time string) []byte { - //if len(time)%2 != 0 { - // time = "0" + time - //} + if len(time)%2 != 0 { + time = "0" + time + } bcd := make([]byte, len(time)/2) for i := 0; i < len(time); i += 2 { // 高4位是第一个字符,低4位是第二个字符 @@ -26,6 +30,48 @@ func Time2BCD(time string) []byte { return bcd } +func CreateVerifyCode(data []byte) byte { + var code byte + for _, v := range data { + code ^= v + } + 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 +} + +func BCD2Time(bcd []byte) string { + result := make([]byte, len(bcd)*2) + for i, v := range bcd { + result[2*i] = (v >> 4) + '0' + result[2*i+1] = (v & 0x0F) + '0' + } + if len(bcd) == 6 { + return fmt.Sprintf("20%s-%s-%s %s:%s:%s", + result[0:2], result[2:4], result[4:6], + result[6:8], result[8:10], result[10:12]) + } + return string(result) +} + +func GBK2UTF8(data []byte) []byte { + utf8Data, _ := io.ReadAll(transform.NewReader(bytes.NewBuffer(data), simplifiedchinese.GBK.NewDecoder())) + return utf8Data +} + +func UTF82GBK(data []byte) []byte { + gbkData, _ := io.ReadAll(transform.NewReader(bytes.NewBuffer(data), simplifiedchinese.GBK.NewEncoder())) + return gbkData +} + func bcdConvert(data []byte) []byte { out := make([]byte, 2*len(data)) index := 0 @@ -58,23 +104,4 @@ func nibbleToHexChar(nibble byte) byte { } // 0x0f bcd编码16进制 只有0-9和a-f return 'f' - -} - -func CreateVerifyCode(data []byte) byte { - var code byte - for _, v := range data { - code ^= v - } - 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 5ff7f1e..f88cd0e 100644 --- a/protocol/utils/utils_test.go +++ b/protocol/utils/utils_test.go @@ -2,6 +2,7 @@ package utils import ( "encoding/hex" + "fmt" "reflect" "testing" ) @@ -91,17 +92,49 @@ func TestCreateVerifyCode(t *testing.T) { } func TestTime2BCD(t *testing.T) { - time := "200707192359" + time := "202410010000" bcd := Time2BCD(time) - want := []byte{32, 7, 7, 25, 35, 89} + want := []byte{32, 36, 16, 1, 0, 0} if !reflect.DeepEqual(bcd, want) { - t.Errorf("Time2BCD() = %x\n want %x", bcd, want) + t.Errorf("Time2BCD() got = %x\n want %x", bcd, want) } } func BenchmarkTime2BCD(b *testing.B) { for i := 0; i < b.N; i++ { - Time2BCD("200707192359") + Time2BCD("20241001000000") + } +} + +func TestBCD2Time(t *testing.T) { + tests := []struct { + name string + args string + want string + }{ + { + name: "6位的时间", + args: "241001000000", + want: "2024-10-01 00:00:00", + }, + { + name: "7位的时间", + args: "20241001000000", + want: "20241001000000", + }, + { + name: "非正常的时间格式", + args: "2024100100000", + want: "02024100100000", // + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + bcd := Time2BCD(tt.args) + if got := BCD2Time(bcd); !reflect.DeepEqual(got, tt.want) { + t.Errorf("TestBCD2Time() got = %s\n want %s", got, tt.want) + } + }) } } @@ -140,3 +173,48 @@ func TestString2FillingBytes(t *testing.T) { }) } } + +func TestEncodingConversion(t *testing.T) { + type args struct { + data []byte + encodingConversion func([]byte) []byte + } + tests := []struct { + name string + args args + want string + }{ + { + name: "gbk -> utf8", + args: args{ + data: []byte{178, 226, 65, 49, 50, 51, 52}, + encodingConversion: GBK2UTF8, + }, + want: "测A1234", + }, + { + name: "utf8 -> gbk", + args: args{ + data: []byte("测A1234"), + encodingConversion: UTF82GBK, + }, + want: string([]byte{178, 226, 65, 49, 50, 51, 52}), + }, + { + name: "utf8 -> gbk 错误的情况", + args: args{ + data: []byte{178, 226, 65, 49, 50, 51, 52}, + encodingConversion: UTF82GBK, + }, + want: "", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fmt.Println(tt.args.encodingConversion(tt.args.data)) + if got := tt.args.encodingConversion(tt.args.data); !reflect.DeepEqual(string(got), tt.want) { + t.Errorf("EncodingConversion() = %s, want %s", string(got), tt.want) + } + }) + } +}