269 lines
8.4 KiB
Go
269 lines
8.4 KiB
Go
package jt808
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"testing"
|
|
|
|
"lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope"
|
|
)
|
|
|
|
const sampleLocationFrame = "7E020000320133077954250001000000000048000301D2C4C707376139000A00E6004F26063016235701040001900C2504000000000202000030011F31010F867E"
|
|
const real2019LocationFrame = "7E02004046010000000001489413506007CB00000000004C000301D276AB06FBFF4B000D014E00E826070122092214040000000017020000010400013361030201402504000000012A02000030011D310112EA0402008000C57E"
|
|
|
|
func TestExtractFramesUnescapesAndParsesLocationWithTotalMileage(t *testing.T) {
|
|
stream, err := hex.DecodeString(sampleLocationFrame + "7E02000000")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
frames, remainder, err := ExtractFrames(stream)
|
|
if err != nil {
|
|
t.Fatalf("ExtractFrames() error = %v", err)
|
|
}
|
|
if len(frames) != 1 {
|
|
t.Fatalf("expected one complete frame, got %d", len(frames))
|
|
}
|
|
if hex.EncodeToString(remainder) != "7e02000000" {
|
|
t.Fatalf("expected partial remainder, got %s", hex.EncodeToString(remainder))
|
|
}
|
|
|
|
env, err := ParseFrame(frames[0], 1782745114999, "115.231.168.135:43625")
|
|
if err != nil {
|
|
t.Fatalf("ParseFrame() error = %v", err)
|
|
}
|
|
if env.Protocol != envelope.ProtocolJT808 {
|
|
t.Fatalf("protocol = %q", env.Protocol)
|
|
}
|
|
if env.MessageID != "0x0200" {
|
|
t.Fatalf("message id = %q", env.MessageID)
|
|
}
|
|
if env.Phone != "013307795425" {
|
|
t.Fatalf("phone = %q", env.Phone)
|
|
}
|
|
if env.Sequence != 1 {
|
|
t.Fatalf("sequence = %d", env.Sequence)
|
|
}
|
|
assertFloatField(t, env, envelope.FieldLatitude, 30.590151)
|
|
assertFloatField(t, env, envelope.FieldLongitude, 121.069881)
|
|
assertFloatField(t, env, envelope.FieldSpeedKMH, 23.0)
|
|
assertFloatField(t, env, envelope.FieldTotalMileageKM, 10241.2)
|
|
if env.Fields["direction_deg"] != uint16(79) {
|
|
t.Fatalf("direction = %#v", env.Fields["direction_deg"])
|
|
}
|
|
|
|
location, ok := env.Parsed["location"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("parsed location missing: %#v", env.Parsed)
|
|
}
|
|
additional, ok := location["additional"].([]map[string]any)
|
|
if !ok || len(additional) == 0 {
|
|
t.Fatalf("additional fields missing: %#v", location["additional"])
|
|
}
|
|
if additional[0]["id"] != "0x01" || additional[0]["value_hex"] != "0001900C" {
|
|
t.Fatalf("unexpected first additional item: %#v", additional[0])
|
|
}
|
|
}
|
|
|
|
func TestParseFrameSupportsJT8082019VersionedHeader(t *testing.T) {
|
|
stream, err := hex.DecodeString(real2019LocationFrame)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
frames, remainder, err := ExtractFrames(stream)
|
|
if err != nil {
|
|
t.Fatalf("ExtractFrames() error = %v", err)
|
|
}
|
|
if len(frames) != 1 || len(remainder) != 0 {
|
|
t.Fatalf("unexpected split result frames=%d remainder=%s", len(frames), hex.EncodeToString(remainder))
|
|
}
|
|
|
|
env, err := ParseFrame(frames[0], 1782914967713, "115.231.168.135:43625")
|
|
if err != nil {
|
|
t.Fatalf("ParseFrame() error = %v", err)
|
|
}
|
|
if env.MessageID != "0x0200" {
|
|
t.Fatalf("message id = %q", env.MessageID)
|
|
}
|
|
if env.Phone != "14894135060" {
|
|
t.Fatalf("phone = %q", env.Phone)
|
|
}
|
|
if env.Sequence != 1995 {
|
|
t.Fatalf("sequence = %d", env.Sequence)
|
|
}
|
|
assertFloatField(t, env, envelope.FieldLatitude, 30.570155)
|
|
assertFloatField(t, env, envelope.FieldLongitude, 117.178187)
|
|
assertFloatField(t, env, envelope.FieldSpeedKMH, 33.4)
|
|
assertFloatField(t, env, envelope.FieldTotalMileageKM, 7868.9)
|
|
if env.Fields["altitude_m"] != uint16(13) {
|
|
t.Fatalf("altitude = %#v", env.Fields["altitude_m"])
|
|
}
|
|
if env.Fields["direction_deg"] != uint16(232) {
|
|
t.Fatalf("direction = %#v", env.Fields["direction_deg"])
|
|
}
|
|
|
|
header, ok := env.Parsed["header"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("parsed header missing: %#v", env.Parsed)
|
|
}
|
|
if header["versioned"] != true || header["protocol_version"] != byte(1) {
|
|
t.Fatalf("unexpected versioned header: %#v", header)
|
|
}
|
|
if header["phone_bcd_hex"] != "00000000014894135060" {
|
|
t.Fatalf("phone bcd = %#v", header["phone_bcd_hex"])
|
|
}
|
|
}
|
|
|
|
func TestParseFrameParsesRegistrationIdentity(t *testing.T) {
|
|
body := make([]byte, 0, 46)
|
|
body = binary.BigEndian.AppendUint16(body, 16)
|
|
body = binary.BigEndian.AppendUint16(body, 32)
|
|
body = appendFixedASCII(body, "YUTNG", 5)
|
|
body = appendFixedASCII(body, "ZK6105CHEVNPG4", 20)
|
|
body = appendFixedASCII(body, "DEV0001", 7)
|
|
body = append(body, 2)
|
|
body = append(body, []byte("TEST123")...)
|
|
|
|
env, err := ParseFrame(buildFrame(0x0100, "013079963379", 7, body), 1782918600000, "115.231.168.135:43625")
|
|
if err != nil {
|
|
t.Fatalf("ParseFrame() error = %v", err)
|
|
}
|
|
if env.MessageID != "0x0100" {
|
|
t.Fatalf("message id = %q", env.MessageID)
|
|
}
|
|
if env.Phone != "013079963379" {
|
|
t.Fatalf("phone = %q", env.Phone)
|
|
}
|
|
if env.DeviceID != "DEV0001" {
|
|
t.Fatalf("device id = %q", env.DeviceID)
|
|
}
|
|
if env.Plate != "TEST123" {
|
|
t.Fatalf("plate = %q", env.Plate)
|
|
}
|
|
registration, ok := env.Parsed["registration"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("registration missing: %#v", env.Parsed)
|
|
}
|
|
if registration["manufacturer"] != "YUTNG" || registration["device_type"] != "ZK6105CHEVNPG4" {
|
|
t.Fatalf("registration fields = %#v", registration)
|
|
}
|
|
if registration["plate_color"] != uint8(2) {
|
|
t.Fatalf("plate color = %#v", registration["plate_color"])
|
|
}
|
|
}
|
|
|
|
func TestParseFrameDecodesGBKRegistrationPlate(t *testing.T) {
|
|
body := make([]byte, 0, 46)
|
|
body = binary.BigEndian.AppendUint16(body, 16)
|
|
body = binary.BigEndian.AppendUint16(body, 32)
|
|
body = appendFixedASCII(body, "YUTNG", 5)
|
|
body = appendFixedASCII(body, "ZK6105CHEVNPG4", 20)
|
|
body = appendFixedASCII(body, "DEV0002", 7)
|
|
body = append(body, 2)
|
|
body = append(body, []byte{0xBB, 0xA6, 'A', '5', '3', '3', '0', '1'}...)
|
|
|
|
env, err := ParseFrame(buildFrame(0x0100, "013079963380", 9, body), 1782918600000, "115.231.168.135:43625")
|
|
if err != nil {
|
|
t.Fatalf("ParseFrame() error = %v", err)
|
|
}
|
|
if env.Plate != "沪A53301" {
|
|
t.Fatalf("plate = %q", env.Plate)
|
|
}
|
|
}
|
|
|
|
func TestParseFrameParsesAuthenticationToken(t *testing.T) {
|
|
env, err := ParseFrame(buildFrame(0x0102, "064646848757", 8, []byte("g7gps")), 1782918600000, "115.231.168.135:43625")
|
|
if err != nil {
|
|
t.Fatalf("ParseFrame() error = %v", err)
|
|
}
|
|
if env.MessageID != "0x0102" {
|
|
t.Fatalf("message id = %q", env.MessageID)
|
|
}
|
|
auth, ok := env.Parsed["authentication"].(map[string]any)
|
|
if !ok {
|
|
t.Fatalf("authentication missing: %#v", env.Parsed)
|
|
}
|
|
if auth["token"] != "g7gps" {
|
|
t.Fatalf("token = %#v", auth["token"])
|
|
}
|
|
}
|
|
|
|
func TestExtractFramesHandlesEscapedPayload(t *testing.T) {
|
|
payload := []byte{0x02, 0x00, 0x00, 0x02, 0x01, 0x33, 0x07, 0x79, 0x54, 0x25, 0x00, 0x01, 0x7e, 0x7d}
|
|
frame := append([]byte{0x7e}, escape(append(payload, checksum(payload)))...)
|
|
frame = append(frame, 0x7e)
|
|
|
|
frames, remainder, err := ExtractFrames(frame)
|
|
if err != nil {
|
|
t.Fatalf("ExtractFrames() error = %v", err)
|
|
}
|
|
if len(frames) != 1 || len(remainder) != 0 {
|
|
t.Fatalf("unexpected split result frames=%d remainder=%s", len(frames), hex.EncodeToString(remainder))
|
|
}
|
|
if hex.EncodeToString(frames[0][:len(payload)]) != hex.EncodeToString(payload) {
|
|
t.Fatalf("frame was not unescaped: %s", hex.EncodeToString(frames[0]))
|
|
}
|
|
}
|
|
|
|
func buildFrame(messageID uint16, phone string, sequence uint16, body []byte) []byte {
|
|
payload := make([]byte, 0, 12+len(body)+1)
|
|
payload = binary.BigEndian.AppendUint16(payload, messageID)
|
|
payload = binary.BigEndian.AppendUint16(payload, uint16(len(body)))
|
|
payload = append(payload, encodeBCD(phone, 6)...)
|
|
payload = binary.BigEndian.AppendUint16(payload, sequence)
|
|
payload = append(payload, body...)
|
|
payload = append(payload, checksum(payload))
|
|
return payload
|
|
}
|
|
|
|
func appendFixedASCII(out []byte, value string, size int) []byte {
|
|
start := len(out)
|
|
out = append(out, make([]byte, size)...)
|
|
copy(out[start:], []byte(value))
|
|
return out
|
|
}
|
|
|
|
func encodeBCD(value string, size int) []byte {
|
|
out := make([]byte, size)
|
|
if len(value)%2 == 1 {
|
|
value = "0" + value
|
|
}
|
|
if len(value) > size*2 {
|
|
value = value[len(value)-size*2:]
|
|
}
|
|
for len(value) < size*2 {
|
|
value = "0" + value
|
|
}
|
|
for i := 0; i < size; i++ {
|
|
out[i] = (value[i*2]-'0')<<4 | (value[i*2+1] - '0')
|
|
}
|
|
return out
|
|
}
|
|
|
|
func assertFloatField(t *testing.T, env envelope.FrameEnvelope, key string, want float64) {
|
|
t.Helper()
|
|
got, ok := env.Fields[key].(float64)
|
|
if !ok {
|
|
t.Fatalf("field %s missing or not float64: %#v", key, env.Fields[key])
|
|
}
|
|
if got != want {
|
|
t.Fatalf("field %s = %v, want %v", key, got, want)
|
|
}
|
|
}
|
|
|
|
func escape(payload []byte) []byte {
|
|
var out []byte
|
|
for _, value := range payload {
|
|
switch value {
|
|
case 0x7e:
|
|
out = append(out, 0x7d, 0x02)
|
|
case 0x7d:
|
|
out = append(out, 0x7d, 0x01)
|
|
default:
|
|
out = append(out, value)
|
|
}
|
|
}
|
|
return out
|
|
}
|