package jt808 import ( "encoding/binary" "encoding/hex" "testing" "lingniu-vehicle-ingest/go/vehicle-gateway/internal/envelope" ) const sampleLocationFrame = "7E020000320133077954250001000000000048000301D2C4C707376139000A00E6004F26063016235701040001900C2504000000000202000030011F31010F867E" const real2019LocationFrame = "7E02004046010000000001489413506007CB00000000004C000301D276AB06FBFF4B000D014E00E826070122092214040000000017020000010400013361030201402504000000012A02000030011D310112EA0402008000C57E" const fullAdditionalLocationFrame = "7e020000800123456789017fff000004000000080006eeb6ad02633df701380003006320070719235901040000000b02020016030200210402002c051e3737370000000000000000000000000000000000000000000000000000001105420000004212064d0000004d4d1307000000580058582504000000632a02000a2b040000001430011e31012806020001927e" 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]) } parsed, ok := additional[0]["parsed"].(map[string]any) if !ok { t.Fatalf("mileage parsed field missing: %#v", additional[0]) } if parsed["raw_value_tenth_km"] != uint32(102412) || parsed["value"] != 10241.2 { t.Fatalf("unexpected mileage parsed value: %#v", parsed) } } 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 TestParseFrameParsesCommonLocationAdditionalItems(t *testing.T) { stream, err := hex.DecodeString(fullAdditionalLocationFrame) 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, "127.0.0.1:808") if err != nil { t.Fatalf("ParseFrame() error = %v", err) } if env.Phone != "012345678901" { t.Fatalf("phone = %q", env.Phone) } assertFloatField(t, env, envelope.FieldTotalMileageKM, 1.1) location := env.Parsed["location"].(map[string]any) additional := location["additional"].([]map[string]any) byID := map[string]map[string]any{} for _, item := range additional { byID[item["id"].(string)] = item["parsed"].(map[string]any) } if byID["0x01"]["raw_value_tenth_km"] != uint32(11) || byID["0x01"]["value"] != 1.1 { t.Fatalf("mileage parsed = %#v", byID["0x01"]) } if byID["0x05"]["name"] != "tire_pressure" { t.Fatalf("tire pressure missing: %#v", byID["0x05"]) } tireValues := byID["0x05"]["values"].([]map[string]any) if tireValues[0]["value"] != byte(0x37) || tireValues[0]["valid"] != true { t.Fatalf("tire pressure first value = %#v", tireValues[0]) } if byID["0x11"]["location_type"] != byte(0x42) || byID["0x11"]["area_id"] != uint32(0x42) { t.Fatalf("overspeed parsed = %#v", byID["0x11"]) } if byID["0x12"]["location_type"] != byte(0x4d) || byID["0x12"]["area_id"] != uint32(0x4d) || byID["0x12"]["direction"] != byte(0x4d) { t.Fatalf("area route parsed = %#v", byID["0x12"]) } if byID["0x13"]["road_section_id"] != uint32(0x58) || byID["0x13"]["driving_time_seconds"] != uint16(0x58) { t.Fatalf("road section parsed = %#v", byID["0x13"]) } ioBits := byID["0x2A"]["bits"].(map[string]bool) if ioBits["deep_sleep"] || !ioBits["sleep"] { t.Fatalf("io bits = %#v", ioBits) } } func TestParseFrameRejectsTruncatedSubpackageHeader(t *testing.T) { payload := []byte{0x02, 0x00, 0x20, 0x00} payload = append(payload, encodeBCD("013307795425", 6)...) payload = binary.BigEndian.AppendUint16(payload, 1) payload = append(payload, checksum(payload)) _, err := ParseFrame(payload, 1782914967713, "115.231.168.135:43625") if err == nil { t.Fatal("expected truncated subpackage header to fail") } } func TestParseLocationAppliesSouthWestStatusBits(t *testing.T) { body := make([]byte, 28) binary.BigEndian.PutUint32(body[4:8], (1<<2)|(1<<3)) binary.BigEndian.PutUint32(body[8:12], 12345678) binary.BigEndian.PutUint32(body[12:16], 98765432) location, err := parseLocation(body) if err != nil { t.Fatalf("parseLocation() error = %v", err) } if location.Latitude != -12.345678 { t.Fatalf("latitude = %v", location.Latitude) } if location.Longitude != -98.765432 { t.Fatalf("longitude = %v", location.Longitude) } } 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 TestParseFrameParsesJT8082019RegistrationIdentity(t *testing.T) { body := make([]byte, 0, 84) body = binary.BigEndian.AppendUint16(body, 16) body = binary.BigEndian.AppendUint16(body, 32) body = appendFixedASCII(body, "YUTONG00001", 11) body = appendFixedASCII(body, "ZK6105CHEVNPG4-2019", 30) body = appendFixedASCII(body, "DEV201900000000000000000000001", 30) body = append(body, 2) body = append(body, []byte("TEST2019")...) env, err := ParseFrame(buildVersionedFrame(0x0100, "14894135060", 11, body), 1782918600000, "115.231.168.135:43625") if err != nil { t.Fatalf("ParseFrame() error = %v", err) } if env.Phone != "14894135060" { t.Fatalf("phone = %q", env.Phone) } if env.DeviceID != "DEV201900000000000000000000001" { t.Fatalf("device id = %q", env.DeviceID) } if env.Plate != "TEST2019" { 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["schema_version"] != "2019" || registration["manufacturer"] != "YUTONG00001" { t.Fatalf("registration fields = %#v", registration) } } 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 TestParseFrameParsesJT8082019Authentication(t *testing.T) { body := []byte{5} body = append(body, []byte("TOKEN")...) body = appendFixedASCII(body, "123456789012345", 15) body = append(body, 4) body = append(body, []byte("v1.2")...) env, err := ParseFrame(buildVersionedFrame(0x0102, "14894135060", 12, body), 1782918600000, "115.231.168.135:43625") if err != nil { t.Fatalf("ParseFrame() error = %v", err) } auth, ok := env.Parsed["authentication"].(map[string]any) if !ok { t.Fatalf("authentication missing: %#v", env.Parsed) } if auth["token"] != "TOKEN" || auth["imei"] != "123456789012345" || auth["software_version"] != "v1.2" { t.Fatalf("authentication = %#v", auth) } } 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 TestAutoResponderBuildsRegisterAck(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")...) request := buildFrame(0x0100, "013079963379", 9, body) env, err := ParseFrame(request, 1782918600000, "115.231.168.135:43625") if err != nil { t.Fatal(err) } response, ok, err := NewAutoResponder("g7gps").Respond(request, env) if err != nil { t.Fatalf("Respond() error = %v", err) } if !ok { t.Fatal("expected response") } frames, remainder, err := ExtractFrames(response) if err != nil { t.Fatalf("ExtractFrames() error = %v", err) } if len(frames) != 1 || len(remainder) != 0 { t.Fatalf("unexpected response split frames=%d remainder=%s", len(frames), hex.EncodeToString(remainder)) } payload := frames[0] if binary.BigEndian.Uint16(payload[0:2]) != 0x8100 { t.Fatalf("message id = %04x", binary.BigEndian.Uint16(payload[0:2])) } if hex.EncodeToString(payload[4:10]) != "013079963379" { t.Fatalf("phone bcd = %x", payload[4:10]) } if binary.BigEndian.Uint16(payload[12:14]) != 9 || payload[14] != 0 || string(payload[15:20]) != "g7gps" { t.Fatalf("unexpected register ack body: %x", payload[12:len(payload)-1]) } } func TestAutoResponderBuildsVersionedGeneralAck(t *testing.T) { request := buildVersionedFrame(0x0200, "14894135060", 12, make([]byte, 28)) env, err := ParseFrame(request, 1782918600000, "115.231.168.135:43625") if err != nil { t.Fatal(err) } response, ok, err := NewAutoResponder("").Respond(request, env) if err != nil { t.Fatalf("Respond() error = %v", err) } if !ok { t.Fatal("expected response") } frames, remainder, err := ExtractFrames(response) if err != nil { t.Fatalf("ExtractFrames() error = %v", err) } if len(frames) != 1 || len(remainder) != 0 { t.Fatalf("unexpected response split frames=%d remainder=%s", len(frames), hex.EncodeToString(remainder)) } payload := frames[0] if binary.BigEndian.Uint16(payload[0:2]) != 0x8001 { t.Fatalf("message id = %04x", binary.BigEndian.Uint16(payload[0:2])) } if binary.BigEndian.Uint16(payload[2:4])&0x4000 == 0 || payload[4] != 1 { t.Fatalf("versioned header not preserved: %x", payload[:5]) } if hex.EncodeToString(payload[5:15]) != "00000000014894135060" { t.Fatalf("phone bcd = %x", payload[5:15]) } bodyStart := 17 if binary.BigEndian.Uint16(payload[bodyStart:bodyStart+2]) != 12 || binary.BigEndian.Uint16(payload[bodyStart+2:bodyStart+4]) != 0x0200 || payload[bodyStart+4] != 0 { t.Fatalf("unexpected general ack body: %x", payload[bodyStart:len(payload)-1]) } } 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 buildVersionedFrame(messageID uint16, phone string, sequence uint16, body []byte) []byte { payload := make([]byte, 0, 17+len(body)+1) payload = binary.BigEndian.AppendUint16(payload, messageID) payload = binary.BigEndian.AppendUint16(payload, 0x4000|uint16(len(body))) payload = append(payload, 1) payload = append(payload, encodeBCD(phone, 10)...) 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 }