diff --git a/go/vehicle-gateway/internal/protocol/jt808/parser.go b/go/vehicle-gateway/internal/protocol/jt808/parser.go index 65df8fb0..6516a648 100644 --- a/go/vehicle-gateway/internal/protocol/jt808/parser.go +++ b/go/vehicle-gateway/internal/protocol/jt808/parser.go @@ -331,45 +331,85 @@ func parseAdditional(data []byte, location *Location) []AdditionalItem { } switch { case id == 0x01 && size == 4: - mileage := float64(binary.BigEndian.Uint32(value)) / 10 + rawMileage := binary.BigEndian.Uint32(value) + mileage := float64(rawMileage) / 10 location.TotalMileageKM = &mileage item.Parsed = map[string]any{ - "name": envelope.FieldTotalMileageKM, - "value": mileage, - "unit": "km", + "name": envelope.FieldTotalMileageKM, + "raw_value_tenth_km": rawMileage, + "value": mileage, + "unit": "km", } case id == 0x02 && size == 2: + rawFuel := binary.BigEndian.Uint16(value) item.Parsed = map[string]any{ - "name": "fuel_l", - "value": float64(binary.BigEndian.Uint16(value)) / 10, - "unit": "L", + "name": "fuel_l", + "raw_value_tenth_l": rawFuel, + "value": float64(rawFuel) / 10, + "unit": "L", } case id == 0x03 && size == 2: + rawSpeed := binary.BigEndian.Uint16(value) item.Parsed = map[string]any{ - "name": "recorder_speed_kmh", - "value": float64(binary.BigEndian.Uint16(value)) / 10, - "unit": "km/h", + "name": "recorder_speed_kmh", + "raw_value_tenth_kmh": rawSpeed, + "value": float64(rawSpeed) / 10, + "unit": "km/h", } case id == 0x04 && size == 2: item.Parsed = map[string]any{ "name": "manual_alarm_event_id", "value": binary.BigEndian.Uint16(value), } + case id == 0x05 && size == 30: + item.Parsed = map[string]any{ + "name": "tire_pressure", + "values": parseTirePressure(value), + } case id == 0x06 && size == 2: item.Parsed = map[string]any{ "name": "carriage_temperature_c", "value": int16(binary.BigEndian.Uint16(value)), "unit": "C", } + case id == 0x11 && (size == 1 || size == 5): + parsed := map[string]any{ + "name": "overspeed_alarm", + "location_type": value[0], + } + if size == 5 { + parsed["area_id"] = binary.BigEndian.Uint32(value[1:5]) + } + item.Parsed = parsed + case id == 0x12 && size == 6: + item.Parsed = map[string]any{ + "name": "area_route_alarm", + "location_type": value[0], + "area_id": binary.BigEndian.Uint32(value[1:5]), + "direction": value[5], + } + case id == 0x13 && size == 7: + item.Parsed = map[string]any{ + "name": "road_section_driving_time_alarm", + "road_section_id": binary.BigEndian.Uint32(value[0:4]), + "driving_time_seconds": binary.BigEndian.Uint16(value[4:6]), + "result": value[6], + "result_description": roadSectionDrivingTimeResult(value[6]), + "driving_time_result_source": "jt808_0x0200_additional_0x13", + } case id == 0x25 && size == 4: + rawStatus := binary.BigEndian.Uint32(value) item.Parsed = map[string]any{ "name": "extended_vehicle_signal_status", - "value": binary.BigEndian.Uint32(value), + "value": rawStatus, + "bits": parseExtendedVehicleSignal(rawStatus), } case id == 0x2a && size == 2: + rawStatus := binary.BigEndian.Uint16(value) item.Parsed = map[string]any{ "name": "io_status", - "value": binary.BigEndian.Uint16(value), + "value": rawStatus, + "bits": parseIOStatus(rawStatus), } case id == 0x2b && size == 4: analog := binary.BigEndian.Uint32(value) @@ -432,6 +472,55 @@ func additionalToMaps(items []AdditionalItem) []map[string]any { return out } +func parseTirePressure(data []byte) []map[string]any { + out := make([]map[string]any, 0, len(data)) + for index, value := range data { + out = append(out, map[string]any{ + "position": index, + "value": value, + "valid": value != 0xff, + }) + } + return out +} + +func parseExtendedVehicleSignal(value uint32) map[string]bool { + return map[string]bool{ + "low_beam": value&0x0001 != 0, + "high_beam": value&0x0002 != 0, + "right_turn": value&0x0004 != 0, + "left_turn": value&0x0008 != 0, + "brake": value&0x0010 != 0, + "reverse_gear": value&0x0020 != 0, + "fog_light": value&0x0040 != 0, + "clearance_light": value&0x0080 != 0, + "horn": value&0x0100 != 0, + "air_conditioner": value&0x0200 != 0, + "neutral": value&0x0400 != 0, + "retarder": value&0x0800 != 0, + "abs": value&0x1000 != 0, + "heater": value&0x2000 != 0, + "clutch": value&0x4000 != 0, + } +} + +func parseIOStatus(value uint16) map[string]bool { + return map[string]bool{ + "deep_sleep": value&0x0001 != 0, + "sleep": value&0x0002 != 0, + } +} + +func roadSectionDrivingTimeResult(value byte) string { + if value == 0 { + return "insufficient" + } + if value == 1 { + return "too_long" + } + return "unknown" +} + func bcdString(data []byte) string { out := make([]byte, 0, len(data)*2) for _, value := range data { diff --git a/go/vehicle-gateway/internal/protocol/jt808/parser_test.go b/go/vehicle-gateway/internal/protocol/jt808/parser_test.go index fb1e2b24..63f5ffff 100644 --- a/go/vehicle-gateway/internal/protocol/jt808/parser_test.go +++ b/go/vehicle-gateway/internal/protocol/jt808/parser_test.go @@ -10,6 +10,7 @@ import ( const sampleLocationFrame = "7E020000320133077954250001000000000048000301D2C4C707376139000A00E6004F26063016235701040001900C2504000000000202000030011F31010F867E" const real2019LocationFrame = "7E02004046010000000001489413506007CB00000000004C000301D276AB06FBFF4B000D014E00E826070122092214040000000017020000010400013361030201402504000000012A02000030011D310112EA0402008000C57E" +const fullAdditionalLocationFrame = "7e020000800123456789017fff000004000000080006eeb6ad02633df701380003006320070719235901040000000b02020016030200210402002c051e3737370000000000000000000000000000000000000000000000000000001105420000004212064d0000004d4d1307000000580058582504000000632a02000a2b040000001430011e31012806020001927e" func TestExtractFramesUnescapesAndParsesLocationWithTotalMileage(t *testing.T) { stream, err := hex.DecodeString(sampleLocationFrame + "7E02000000") @@ -63,6 +64,13 @@ func TestExtractFramesUnescapesAndParsesLocationWithTotalMileage(t *testing.T) { 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) { @@ -115,6 +123,59 @@ func TestParseFrameSupportsJT8082019VersionedHeader(t *testing.T) { } } +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)...)